diff --git a/assets/ui/cercle-jaune-checked.png b/assets/ui/cercle-jaune-checked.png
deleted file mode 100644
index 3e5e437..0000000
Binary files a/assets/ui/cercle-jaune-checked.png and /dev/null differ
diff --git a/assets/ui/cercle-jaune-unchecked.png b/assets/ui/cercle-jaune-unchecked.png
deleted file mode 100644
index 2ea6ed7..0000000
Binary files a/assets/ui/cercle-jaune-unchecked.png and /dev/null differ
diff --git a/assets/ui/cercle-vert-checked.png b/assets/ui/cercle-vert-checked.png
deleted file mode 100644
index 8ff9ff9..0000000
Binary files a/assets/ui/cercle-vert-checked.png and /dev/null differ
diff --git a/assets/ui/cercle-vert-unchecked.png b/assets/ui/cercle-vert-unchecked.png
deleted file mode 100644
index 4fdc9f6..0000000
Binary files a/assets/ui/cercle-vert-unchecked.png and /dev/null differ
diff --git a/fvtt-celestopol.mjs b/fvtt-celestopol.mjs
index 9c06d1a..4537a5c 100644
--- a/fvtt-celestopol.mjs
+++ b/fvtt-celestopol.mjs
@@ -68,7 +68,7 @@ Hooks.once("init", () => {
// ── Sheets: unregister core, register system sheets ─────────────────────
foundry.applications.sheets.ActorSheetV2.unregisterSheet?.("core", "Actor", { types: ["character", "npc"] })
- foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
+ foundry.appv1?.sheets?.ActorSheet && foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, CelestopolCharacterSheet, {
types: ["character"],
makeDefault: true,
@@ -80,7 +80,7 @@ Hooks.once("init", () => {
label: "CELESTOPOL.Sheet.npc",
})
- foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
+ foundry.appv1?.sheets?.ItemSheet && foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolAnomalySheet, {
types: ["anomaly"],
makeDefault: true,
@@ -130,6 +130,7 @@ Hooks.once("ready", () => {
// Migration : supprime les items de types obsolètes (ex: "attribute")
if (game.user.isGM) {
_migrateObsoleteItems()
+ _migrateIntegerTracks()
}
})
@@ -156,6 +157,68 @@ async function _migrateObsoleteItems() {
}
}
+/**
+ * Migration : convertit les anciennes données booléennes (level1..level8, b1..b8, etc.)
+ * vers le nouveau stockage entier direct.
+ * Ne s'applique qu'aux acteurs ayant encore l'ancien format dans leur source.
+ */
+async function _migrateIntegerTracks() {
+ const validActors = game.actors.contents.filter(a => ["character", "npc"].includes(a.type))
+
+ for (const actor of validActors) {
+ const src = actor._source?.system
+ if (!src) continue
+
+ const updateData = {}
+
+ // Blessures : si b1 existe dans la source, recalculer lvl depuis les booléens
+ const blessures = src.blessures ?? {}
+ if ("b1" in blessures) {
+ const lvl = [1,2,3,4,5,6,7,8].filter(i => blessures[`b${i}`]?.checked === true).length
+ updateData["system.blessures.lvl"] = lvl
+ }
+
+ if (actor.type === "character") {
+ // Destin
+ const destin = src.destin ?? {}
+ if ("d1" in destin) {
+ const lvl = [1,2,3,4,5,6,7,8].filter(i => destin[`d${i}`]?.checked === true).length
+ updateData["system.destin.lvl"] = lvl
+ }
+
+ // Spleen
+ const spleen = src.spleen ?? {}
+ if ("s1" in spleen) {
+ const lvl = [1,2,3,4,5,6,7,8].filter(i => spleen[`s${i}`]?.checked === true).length
+ updateData["system.spleen.lvl"] = lvl
+ }
+
+ // Domaines : si level1 existe dans un domaine, recalculer value depuis les booléens
+ const stats = src.stats ?? {}
+ for (const [statId, statData] of Object.entries(stats)) {
+ for (const [skillId, skill] of Object.entries(statData ?? {})) {
+ if (typeof skill !== "object" || !("level1" in skill)) continue
+ const value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`] === true).length
+ updateData[`system.stats.${statId}.${skillId}.value`] = value
+ }
+ }
+
+ // Factions : si level1 existe dans une faction, recalculer value depuis les booléens
+ const factions = src.factions ?? {}
+ for (const [factionId, faction] of Object.entries(factions)) {
+ if (typeof faction !== "object" || !("level1" in faction)) continue
+ const value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`] === true).length
+ updateData[`system.factions.${factionId}.value`] = value
+ }
+ }
+
+ if (Object.keys(updateData).length > 0) {
+ console.log(`${SYSTEM_ID} | Migration tracks → entiers : ${actor.name}`, updateData)
+ await actor.update(updateData)
+ }
+ }
+}
+
/* ─── Handlebars helpers ─────────────────────────────────────────────────── */
function _registerHandlebarsHelpers() {
@@ -183,6 +246,9 @@ function _registerHandlebarsHelpers() {
// Helper : build array from args (Handlebars doesn't have arrays natively)
Handlebars.registerHelper("array", (...args) => args.slice(0, -1))
+ // Helper : range(n) → [1, 2, ..., n] — pour les boucles de cases à cocher
+ Handlebars.registerHelper("range", (n) => Array.from({ length: n }, (_, i) => i + 1))
+
// Helper : nested object lookup with dot path or multiple keys
Handlebars.registerHelper("lookup", (obj, ...args) => {
const options = args.pop() // last arg is Handlebars options hash
diff --git a/lang/fr.json b/lang/fr.json
index dbcde01..562ce66 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -95,29 +95,40 @@
"destin": "Destin",
"spleen": "Spleen",
"level": "Niveau",
- "currentMalus": "Malus actuel"
+ "currentMalus": "Malus actuel",
+ "destinTooltip": "Usages du Destin :\n• Réaliser un test avec 3d8\n• Gagner l'initiative lors d'un combat\n• Trouver l'ensemble des indices\n• Éviter une blessure\n• Sortir de l'inconscience\n• Obtenir un Triomphe"
},
"Wound": {
- "none": "Aucune blessure",
- "anodin": "Anodin",
- "derisoire": "Dérisoire",
- "negligeable": "Négligeable",
- "superficiel": "Superficiel",
- "leger": "Léger",
- "modere": "Modéré",
- "grave": "Grave",
- "dramatique": "Dramatique (hors combat)"
+ "none": "Aucune blessure",
+ "anodin": "Anodin",
+ "derisoire": "Dérisoire",
+ "negligeable": "Négligeable",
+ "superficiel": "Superficiel",
+ "leger": "Léger",
+ "modere": "Modéré",
+ "grave": "Grave",
+ "dramatique": "Dramatique (hors combat)",
+ "duration1min": "1 min",
+ "duration10min": "10 min",
+ "duration30min": "30 min",
+ "duration1jour": "1 journée",
+ "status": "État : "
},
"Combat": {
- "attack": "Attaquer",
- "corpsPnj": "Corps du PNJ",
- "tie": "ÉGALITÉ",
- "tieDesc": "Personne n'est blessé",
- "successHit": "PNJ touché — 1 blessure",
- "failureHit": "Joueur touché — 1 blessure (mêlée)",
- "distanceNoWound": "Raté — pas de riposte",
- "weaponDamage": "dégâts supplémentaires",
- "playerWounded": "Blessure infligée au joueur (mêlée)"
+ "attack": "Attaquer",
+ "corpsPnj": "Corps du PNJ",
+ "tie": "ÉGALITÉ",
+ "tieDesc": "Personne n'est blessé",
+ "successHit": "PNJ touché — 1 blessure",
+ "failureHit": "Joueur touché — 1 blessure (mêlée)",
+ "distanceNoWound": "Raté — pas de riposte",
+ "weaponDamage": "dégâts supplémentaires",
+ "playerWounded": "Blessure infligée au joueur (mêlée)",
+ "rangedDefenseTitle": "Esquiver (Mobilité)",
+ "rangedDefenseTag": "Défense à distance",
+ "rangedDefenseSuccess": "Attaque esquivée — pas de blessure",
+ "rangedDefenseFailure": "Touché par le PNJ — 1 blessure",
+ "rangedDefensePlayerWounded":"Blessure infligée par attaque à distance"
},
"Tab": {
"main": "Principal",
@@ -294,6 +305,30 @@
},
"Aspect": {
"valeur": "Valeur"
+ },
+ "XP": {
+ "title": "Expérience",
+ "actuel": "XP disponible",
+ "depense": "XP dépensée",
+ "depenser": "Dépenser XP",
+ "confirmer": "Confirmer",
+ "montant": "Montant",
+ "raison": "Raison",
+ "raisonPlaceholder": "Ex : Augmentation Mobilité à 4",
+ "date": "Date",
+ "supprimer": "Annuler cette dépense",
+ "disponible": "{n} XP disponibles",
+ "insuffisant": "XP insuffisante — seulement {n} disponibles",
+ "montantInvalide": "Le montant doit être supérieur à 0",
+ "refTitle": "Tableau des coûts",
+ "refAmelioration": "Amélioration",
+ "refCout": "Coût (XP)",
+ "refAugmenterSpec": "Augmenter une Spécialisation",
+ "refCoutNiveau": "= niveau à atteindre",
+ "refAcquerirAspect": "Acquérir un nouvel Aspect",
+ "refAugmenterAspect":"Augmenter / Diminuer un Aspect",
+ "refAcquerirAttribut":"Acquérir ou augmenter un Attribut",
+ "refCoutAttributTotal":"= total des points × 10"
}
}
}
\ No newline at end of file
diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs
index 79594bf..05c74f4 100644
--- a/module/applications/sheets/base-actor-sheet.mjs
+++ b/module/applications/sheets/base-actor-sheet.mjs
@@ -1,3 +1,5 @@
+import { SYSTEM } from "../../config/system.mjs"
+
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
@@ -18,11 +20,15 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
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,
+ 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,
},
}
@@ -42,6 +48,7 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable,
+ woundLevels: SYSTEM.WOUND_LEVELS,
}
}
@@ -51,28 +58,9 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
this.element.querySelectorAll(".rollable").forEach(el => {
el.addEventListener("click", this._onRoll.bind(this))
})
-
- // Setup sequential checkbox logic for wound tracks
- this._setupSequentialCheckboxes()
-
- // Setup sequential checkbox logic for factions
- this._setupFactionCheckboxes()
- }
-
- /** @override */
- _onClick(event) {
- // Skip checkbox clicks in edit mode
- if (this.isEditMode && event.target.classList.contains('skill-level-checkbox')) {
- return
- }
- super._onClick(event)
}
async _onRoll(event) {
- // Don't roll if clicking on a checkbox
- if (event.target.classList.contains('skill-level-checkbox')) {
- return
- }
if (!this.isPlayMode) return
const el = event.currentTarget
const statId = el.dataset.statId
@@ -158,132 +146,39 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
await this.document.system.rollAttack(itemId)
}
- /**
- * Setup sequential checkbox logic for wound/destin/spleen tracks
- * Only allows checking the next checkbox in sequence
- */
- _setupSequentialCheckboxes() {
- this.element.querySelectorAll('.wound-checkbox').forEach(checkbox => {
- checkbox.addEventListener('change', (event) => {
- this._handleSequentialCheckboxChange(event)
- })
- })
+ static async #onRangedDefense(_event, target) {
+ const itemId = target.getAttribute("data-item-id")
+ if (!itemId) return
+ await this.document.system.rollRangedDefense(itemId)
}
- /**
- * Handle sequential checkbox change logic
- * @param {Event} event - The change event
- */
- _handleSequentialCheckboxChange(event) {
- const checkbox = event.target
- if (!checkbox.classList.contains('wound-checkbox') || checkbox.disabled) return
-
- const track = checkbox.dataset.track
- const currentIndex = parseInt(checkbox.dataset.index)
- const isChecked = checkbox.checked
-
- // Get all checkboxes in this track
- const trackCheckboxes = Array.from(this.element.querySelectorAll(`.wound-checkbox[data-track="${track}"]`))
-
- if (isChecked) {
- // Checking a box: uncheck all boxes after this one
- for (let i = currentIndex + 1; i < trackCheckboxes.length; i++) {
- trackCheckboxes[i].checked = false
- }
- // Check all boxes before this one
- for (let i = 0; i < currentIndex; i++) {
- trackCheckboxes[i].checked = true
- }
- } else {
- // Unchecking a box: uncheck all boxes after this one
- for (let i = currentIndex; i < trackCheckboxes.length; i++) {
- trackCheckboxes[i].checked = false
- }
- }
-
- // Update the visual state
- this._updateTrackVisualState()
+ /** 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) })
}
- /**
- * Update visual state of track boxes based on checkbox states
- */
- _updateTrackVisualState() {
- this.element.querySelectorAll('.track-box').forEach(box => {
- const checkbox = box.querySelector('.wound-checkbox')
- if (checkbox) {
- if (checkbox.checked) {
- box.classList.add('checked')
- } else {
- box.classList.remove('checked')
- }
- }
- })
+ /** 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) })
}
- /**
- * Setup sequential checkbox logic for faction tracks
- */
- _setupFactionCheckboxes() {
- this.element.querySelectorAll('.faction-checkbox').forEach(checkbox => {
- checkbox.addEventListener('change', (event) => {
- this._handleFactionCheckboxChange(event)
- })
- })
- }
-
- /**
- * Handle faction checkbox change logic
- * @param {Event} event - The change event
- */
- _handleFactionCheckboxChange(event) {
- const checkbox = event.target
- if (!checkbox.classList.contains('faction-checkbox') || checkbox.disabled) return
-
- const factionId = checkbox.dataset.faction
- const currentLevel = parseInt(checkbox.dataset.level)
- const isChecked = checkbox.checked
-
- // Get all checkboxes for this faction
- const factionCheckboxes = Array.from(this.element.querySelectorAll(`.faction-checkbox[data-faction="${factionId}"]`))
-
- if (isChecked) {
- // Checking a box: check all boxes before this one, uncheck all boxes after this one
- for (let i = 0; i < currentLevel; i++) {
- factionCheckboxes[i].checked = true
- }
- for (let i = currentLevel; i < factionCheckboxes.length; i++) {
- factionCheckboxes[i].checked = false
- }
- } else {
- // Unchecking a box: uncheck all boxes after this one
- for (let i = currentLevel - 1; i < factionCheckboxes.length; i++) {
- factionCheckboxes[i].checked = false
- }
- }
-
- // Update the count display
- this._updateFactionCount(factionId)
- }
-
- /**
- * Update the faction count display based on checked checkboxes
- * @param {string} factionId - The faction ID
- */
- _updateFactionCount(factionId) {
- const checkboxes = Array.from(this.element.querySelectorAll(`.faction-checkbox[data-faction="${factionId}"]:checked`))
- const count = checkboxes.length
-
- // Update the hidden input field
- const input = this.element.querySelector(`input[name="system.factions.${factionId}.value"]`)
- if (input) {
- input.value = count
- }
-
- // Update the visual count display
- const countDisplay = this.element.querySelector(`.faction-row[data-faction="${factionId}"] .faction-count`)
- if (countDisplay) {
- countDisplay.textContent = count
- }
+ /** Met à jour le score d'une faction par clic sur un point. */
+ static #onFactionLevel(_event, target) {
+ if (!this.isEditable) return
+ const factionId = target.dataset.faction
+ const index = parseInt(target.dataset.index)
+ const current = this.document.system.factions[factionId]?.value ?? 0
+ const newValue = (index <= current) ? index - 1 : index
+ this.document.update({ [`system.factions.${factionId}.value`]: Math.max(0, newValue) })
}
}
diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs
index 8251c54..5f79d8f 100644
--- a/module/applications/sheets/character-sheet.mjs
+++ b/module/applications/sheets/character-sheet.mjs
@@ -15,6 +15,8 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
createArmure: CelestopolCharacterSheet.#onCreateArmure,
useAnomaly: CelestopolCharacterSheet.#onUseAnomaly,
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
+ depenseXp: CelestopolCharacterSheet.#onDepenseXp,
+ supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
},
}
@@ -95,6 +97,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
case "biography":
context.tab = context.tabs.biography
+ context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.description, { async: true })
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
@@ -111,36 +114,36 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
return context
}
- static #onCreateAnomaly() {
+ static async #onCreateAnomaly() {
if (this.document.itemTypes.anomaly.length > 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
return
}
- this.document.createEmbeddedDocuments("Item", [{
+ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
}])
}
- static #onCreateAspect() {
- this.document.createEmbeddedDocuments("Item", [{
+ static async #onCreateAspect() {
+ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
}])
}
- static #onCreateEquipment() {
- this.document.createEmbeddedDocuments("Item", [{
+ static async #onCreateEquipment() {
+ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.equipment"), type: "equipment",
}])
}
- static #onCreateWeapon() {
- this.document.createEmbeddedDocuments("Item", [{
+ static async #onCreateWeapon() {
+ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon",
}])
}
- static #onCreateArmure() {
- this.document.createEmbeddedDocuments("Item", [{
+ static async #onCreateArmure() {
+ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.armure"), type: "armure",
}])
}
@@ -163,4 +166,73 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
if (!anomaly) return
await anomaly.update({ "system.usesRemaining": anomaly.system.level })
}
+
+ /** Ouvre un dialogue pour dépenser de l'XP. */
+ static async #onDepenseXp() {
+ const actor = this.document
+ const currentXp = actor.system.xp?.actuel ?? 0
+ const i18n = game.i18n
+
+ const content = `
+
`
+
+ const result = await foundry.applications.api.DialogV2.prompt({
+ window: { title: i18n.localize("CELESTOPOL.XP.depenser") },
+ content,
+ ok: {
+ label: i18n.localize("CELESTOPOL.XP.confirmer"),
+ callback: (event, button) => {
+ const form = button.form
+ return {
+ montant: parseInt(form.querySelector("[name=montant]").value) || 0,
+ raison: form.querySelector("[name=raison]").value.trim(),
+ }
+ },
+ },
+ })
+
+ if (!result) return
+ const { montant, raison } = result
+
+ if (montant <= 0) {
+ ui.notifications.warn(i18n.localize("CELESTOPOL.XP.montantInvalide"))
+ return
+ }
+ if (montant > currentXp) {
+ ui.notifications.warn(i18n.format("CELESTOPOL.XP.insuffisant", { n: currentXp }))
+ return
+ }
+
+ const date = new Date().toLocaleDateString("fr-FR")
+ const log = [...(actor.system.xp.log ?? []), { montant, raison, date }]
+ await actor.update({
+ "system.xp.actuel": currentXp - montant,
+ "system.xp.log": log,
+ })
+ }
+
+ /** Supprime une entrée du log XP et rembourse les points (mode édition). */
+ static async #onSupprimerXpLog(event, target) {
+ const idx = parseInt(target.dataset.idx)
+ const actor = this.document
+ const log = [...(actor.system.xp.log ?? [])]
+ if (isNaN(idx) || idx < 0 || idx >= log.length) return
+
+ const entry = log[idx]
+ log.splice(idx, 1)
+ await actor.update({
+ "system.xp.actuel": (actor.system.xp?.actuel ?? 0) + entry.montant,
+ "system.xp.log": log,
+ })
+ }
}
diff --git a/module/config/system.mjs b/module/config/system.mjs
index 85698fd..53e4d9f 100644
--- a/module/config/system.mjs
+++ b/module/config/system.mjs
@@ -90,15 +90,15 @@ export const FACTIONS = {
/** 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 },
+ { id: 0, label: "CELESTOPOL.Wound.none", malus: 0, duration: "" },
+ { id: 1, label: "CELESTOPOL.Wound.anodin", malus: 0, duration: "CELESTOPOL.Wound.duration1min" },
+ { id: 2, label: "CELESTOPOL.Wound.negligeable", malus: 0, duration: "CELESTOPOL.Wound.duration1min" },
+ { id: 3, label: "CELESTOPOL.Wound.derisoire", malus: -1, duration: "CELESTOPOL.Wound.duration10min" },
+ { id: 4, label: "CELESTOPOL.Wound.superficiel", malus: -1, duration: "CELESTOPOL.Wound.duration10min" },
+ { id: 5, label: "CELESTOPOL.Wound.leger", malus: -2, duration: "CELESTOPOL.Wound.duration30min" },
+ { id: 6, label: "CELESTOPOL.Wound.modere", malus: -2, duration: "CELESTOPOL.Wound.duration30min" },
+ { id: 7, label: "CELESTOPOL.Wound.grave", malus: -3, duration: "CELESTOPOL.Wound.duration1jour" },
+ { id: 8, label: "CELESTOPOL.Wound.dramatique", malus: -999, duration: "" },
]
/** Seuils de difficulté pour les jets de dés. */
diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs
index 5cadf79..05f4c55 100644
--- a/module/documents/actor.mjs
+++ b/module/documents/actor.mjs
@@ -1,10 +1,4 @@
export default class CelestopolActor extends Actor {
- /** @override */
- prepareDerivedData() {
- super.prepareDerivedData()
- this.system.prepareDerivedData?.()
- }
-
/** @override */
getRollData() {
return this.toObject(false).system
diff --git a/module/documents/chat-message.mjs b/module/documents/chat-message.mjs
index aa181a9..e63dc12 100644
--- a/module/documents/chat-message.mjs
+++ b/module/documents/chat-message.mjs
@@ -1,6 +1 @@
-export default class CelestopolChatMessage extends ChatMessage {
- /** @override */
- async renderHTML(options = {}) {
- return super.renderHTML(options)
- }
-}
+export default class CelestopolChatMessage extends ChatMessage {}
diff --git a/module/documents/item.mjs b/module/documents/item.mjs
index b96cb45..813e104 100644
--- a/module/documents/item.mjs
+++ b/module/documents/item.mjs
@@ -1,9 +1,4 @@
export default class CelestopolItem extends Item {
- /** @override */
- prepareDerivedData() {
- super.prepareDerivedData()
- }
-
/** @override */
getRollData() {
return this.toObject(false).system
diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs
index cfe465c..970a818 100644
--- a/module/documents/roll.mjs
+++ b/module/documents/roll.mjs
@@ -42,6 +42,7 @@ export class CelestopolRoll extends Roll {
const fortuneValue = options.fortuneValue ?? 0
const isResistance = options.isResistance ?? false
const isCombat = options.isCombat ?? false
+ const isRangedDefense = options.isRangedDefense ?? false
const weaponType = options.weaponType ?? "melee"
const weaponName = options.weaponName ?? null
const weaponDegats = options.weaponDegats ?? "0"
@@ -72,6 +73,7 @@ export class CelestopolRoll extends Roll {
defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
isCombat,
+ isRangedDefense,
weaponType,
weaponName,
weaponDegats,
@@ -221,6 +223,7 @@ export class CelestopolRoll extends Roll {
autoSuccess,
isResistance,
isCombat,
+ isRangedDefense,
weaponType,
weaponName,
weaponDegats,
@@ -243,66 +246,49 @@ export class CelestopolRoll extends Roll {
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure
const actor = game.actors.get(options.actorId)
if (isResistance && actor && roll.options.resultType === "failure") {
- const wounds = actor.system.blessures
- const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
- if (nextIdx) {
- await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
- roll.options.woundTaken = nextIdx
+ const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
+ if (nextLvl <= 8) {
+ await actor.update({ "system.blessures.lvl": nextLvl })
+ roll.options.woundTaken = nextLvl
}
}
- // Combat mêlée échoué → joueur prend une blessure
- if (isCombat && weaponType === "melee" && actor && roll.options.resultType === "failure") {
- const wounds = actor.system.blessures
- const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
- if (nextIdx) {
- await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
- roll.options.woundTaken = nextIdx
+ // Mêlée échouée OU défense à distance échouée → joueur prend une blessure
+ if (isCombat && (weaponType === "melee" || isRangedDefense) && actor && roll.options.resultType === "failure") {
+ const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
+ if (nextLvl <= 8) {
+ await actor.update({ "system.blessures.lvl": nextLvl })
+ roll.options.woundTaken = nextLvl
}
}
await roll.toMessage({}, { rollMode: rollData.rollMode })
- // Destin utilisé → vider la jauge (reset à 0)
- if (rollData.useDestin && actor) {
- await actor.update({
- "system.destin.lvl": 0,
- "system.destin.d1.checked": false,
- "system.destin.d2.checked": false,
- "system.destin.d3.checked": false,
- "system.destin.d4.checked": false,
- "system.destin.d5.checked": false,
- "system.destin.d6.checked": false,
- "system.destin.d7.checked": false,
- "system.destin.d8.checked": false,
- })
- }
-
- // Fortune utilisée → décrémenter de 1 (min 0)
- if (rollData.useFortune && actor) {
- const currentFortune = actor.system.attributs.fortune.value ?? 0
- await actor.update({ "system.attributs.fortune.value": Math.max(0, currentFortune - 1) })
- }
-
- // Puiser dans ses ressources → coche une case de spleen
- if (rollData.puiserRessources && actor) {
- const currentSpleen = actor.system.spleen.lvl ?? 0
- if (currentSpleen < 8) {
- const newLvl = currentSpleen + 1
- const key = `s${newLvl}`
- await actor.update({
- "system.spleen.lvl": newLvl,
- [`system.spleen.${key}.checked`]: true,
- })
- }
- }
-
- // Mémoriser les préférences sur l'acteur
+ // Batching de toutes les mises à jour de l'acteur en un seul appel réseau
if (actor) {
- await actor.update({
- "system.prefs.rollMoonDie": rollData.rollMoonDie,
- "system.prefs.difficulty": difficulty,
- })
+ const updateData = {}
+
+ if (rollData.useDestin) {
+ updateData["system.destin.lvl"] = 0
+ }
+
+ if (rollData.useFortune) {
+ const currentFortune = actor.system.attributs.fortune.value ?? 0
+ updateData["system.attributs.fortune.value"] = Math.max(0, currentFortune - 1)
+ }
+
+ if (rollData.puiserRessources) {
+ const currentSpleen = actor.system.spleen.lvl ?? 0
+ if (currentSpleen < 8) {
+ updateData["system.spleen.lvl"] = currentSpleen + 1
+ }
+ }
+
+ // Mémoriser les préférences
+ updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
+ updateData["system.prefs.difficulty"] = difficulty
+
+ await actor.update(updateData)
}
return roll
@@ -421,6 +407,7 @@ export class CelestopolRoll extends Roll {
weaponName: this.options.weaponName ?? null,
weaponDegats: this.options.weaponDegats ?? null,
weaponType: this.options.weaponType ?? null,
+ isRangedDefense: this.options.isRangedDefense ?? false,
woundTaken: this.options.woundTaken ?? null,
// Dé de lune
hasMoonDie: moonDieResult !== null,
diff --git a/module/models/character.mjs b/module/models/character.mjs
index 4873e42..32618cd 100644
--- a/module/models/character.mjs
+++ b/module/models/character.mjs
@@ -21,18 +21,10 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
})
- // Les 4 stats avec leurs domaines
+ // Les 4 stats avec leurs domaines — niveau stocké directement comme entier
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 }),
- level1: new fields.BooleanField({ required: true, initial: false }),
- level2: new fields.BooleanField({ required: true, initial: false }),
- level3: new fields.BooleanField({ required: true, initial: false }),
- level4: new fields.BooleanField({ required: true, initial: false }),
- level5: new fields.BooleanField({ required: true, initial: false }),
- level6: new fields.BooleanField({ required: true, initial: false }),
- level7: new fields.BooleanField({ required: true, initial: false }),
- level8: new fields.BooleanField({ required: true, initial: false }),
})
const statField = (statId) => {
@@ -55,32 +47,19 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
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 }),
- })
+ // Blessures — niveau entier direct (0 = aucune, 8 = fatale)
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 }),
- })
+ // Destin — jauge entière directe
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)
+ // Spleen — jauge entière directe
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)
@@ -95,18 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
vision: persoAttrField(),
})
- // Factions - 9 checkboxes per faction (like wound tracks)
+ // Factions — score entier direct (0-9)
const factionField = () => new fields.SchemaField({
- value: new fields.NumberField({ ...reqInt, initial: 0 }),
- level1: new fields.BooleanField({ required: true, initial: false }),
- level2: new fields.BooleanField({ required: true, initial: false }),
- level3: new fields.BooleanField({ required: true, initial: false }),
- level4: new fields.BooleanField({ required: true, initial: false }),
- level5: new fields.BooleanField({ required: true, initial: false }),
- level6: new fields.BooleanField({ required: true, initial: false }),
- level7: new fields.BooleanField({ required: true, initial: false }),
- level8: new fields.BooleanField({ required: true, initial: false }),
- level9: new fields.BooleanField({ required: true, initial: false }),
+ value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 9 }),
})
schema.factions = new fields.SchemaField({
pinkerton: factionField(),
@@ -133,6 +103,16 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
})
+ // Expérience
+ schema.xp = new fields.SchemaField({
+ actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
+ log: new fields.ArrayField(new fields.SchemaField({
+ montant: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
+ raison: new fields.StringField({ required: true, nullable: false, initial: "" }),
+ date: new fields.StringField({ required: true, nullable: false, initial: "" }),
+ })),
+ })
+
// Description & notes
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
@@ -156,15 +136,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
prepareDerivedData() {
super.prepareDerivedData()
- // Calcul automatique de la valeur de chaque domaine = nombre de cases cochées
- for (const stat of Object.values(this.stats)) {
- for (const skill of Object.values(stat)) {
- if (typeof skill !== "object" || !("level1" in skill)) continue
- skill.value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`]).length
- }
- }
-
- // Calcul automatique de la Résistance par stat = +2 par domaine atteignant son seuil
+ // Résistance par stat = +2 par domaine atteignant son seuil de spécialisation
for (const [statId, statData] of Object.entries(this.stats)) {
let res = 0
for (const [skillId, skill] of Object.entries(statData)) {
@@ -175,19 +147,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
statData.res = res
}
- // Calcul automatique de la valeur de chaque faction = nombre de cases cochées
- for (const faction of Object.values(this.factions)) {
- if (typeof faction !== "object" || !("level1" in faction)) continue
- faction.value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`]).length
- }
-
- // Calcul automatique du niveau des jauges depuis les cases cochées
- this.blessures.lvl = [1,2,3,4,5,6,7,8].filter(i => this.blessures[`b${i}`]?.checked).length
- this.destin.lvl = [1,2,3,4,5,6,7,8].filter(i => this.destin[`d${i}`]?.checked).length
- this.spleen.lvl = [1,2,3,4,5,6,7,8].filter(i => this.spleen[`s${i}`]?.checked).length
-
- // Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur) [après calcul des domaines]
+ // Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur)
this.initiative = 4 + (this.stats.corps.mobilite?.value ?? 0) + (this.stats.coeur.inspiration?.value ?? 0)
+
+ // XP dépensée = somme des montants du log
+ this.xp.depense = this.xp.log.reduce((sum, entry) => sum + entry.montant, 0)
}
/**
@@ -239,10 +203,29 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
const statData = this.stats[statId]
if (!statData) return null
+ return CelestopolRoll.prompt({
+ actorId: this.parent.id,
+ actorName: this.parent.name,
+ actorImage: this.parent.img,
+ statId,
+ skillId: null,
+ statLabel: SYSTEM.STATS[statId]?.label,
+ skillLabel: "CELESTOPOL.Roll.resistanceTest",
+ skillValue: statData.res,
+ woundMalus: this.getWoundMalus(),
+ woundLevel: this.blessures.lvl,
+ isResistance: true,
+ rollMoonDie: false,
+ destGaugeFull: false,
+ fortuneValue: 0,
+ difficulty: "normal",
+ })
+ }
+
/**
- * Lance une attaque avec une arme (test Échauffourée vs Corps PNJ).
- * Mêlée : échec → blessure joueur auto-cochée.
- * Distance : échec → simple raté, pas de blessure joueur.
+ * Lance une attaque avec une arme.
+ * Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur.
+ * Distance : test Échauffourée vs Corps PNJ ; échec → pas de blessure joueur.
* Égalité (marge=0) → personne n'est blessé.
* @param {string} itemId - Id de l'item arme
*/
@@ -255,23 +238,60 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
if (!echauffouree) return null
return CelestopolRoll.prompt({
- actorId: this.parent.id,
- actorName: this.parent.name,
- actorImage: this.parent.img,
- statId: "corps",
- skillId: "echauffouree",
- statLabel: SYSTEM.STATS.corps.label,
- skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
- skillValue: echauffouree.value,
- woundMalus: this.getWoundMalus(),
- woundLevel: this.blessures.lvl,
- rollMoonDie: this.prefs.rollMoonDie ?? false,
- destGaugeFull: this.destin.lvl > 0,
- fortuneValue: this.attributs.fortune.value,
- isCombat: true,
- weaponType: item.system.type,
- weaponName: item.name,
- weaponDegats: item.system.degats,
+ actorId: this.parent.id,
+ actorName: this.parent.name,
+ actorImage: this.parent.img,
+ statId: "corps",
+ skillId: "echauffouree",
+ statLabel: SYSTEM.STATS.corps.label,
+ skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
+ skillValue: echauffouree.value,
+ woundMalus: this.getWoundMalus(),
+ woundLevel: this.blessures.lvl,
+ rollMoonDie: this.prefs.rollMoonDie ?? false,
+ destGaugeFull: this.destin.lvl > 0,
+ fortuneValue: this.attributs.fortune.value,
+ isCombat: true,
+ isRangedDefense: false,
+ weaponType: item.system.type,
+ weaponName: item.name,
+ weaponDegats: item.system.degats,
+ })
+ }
+
+ /**
+ * Lance un jet de défense contre une attaque à distance (test Mobilité vs Corps PNJ).
+ * Succès → esquive réussie.
+ * Échec → blessure automatique (le PNJ touche).
+ * @param {string} itemId - Id de l'item arme (distance uniquement)
+ */
+ async rollRangedDefense(itemId) {
+ const { CelestopolRoll } = await import("../documents/roll.mjs")
+ const item = this.parent.items.get(itemId)
+ if (!item || item.type !== "weapon" || item.system.type !== "distance") return null
+
+ const mobilite = this.stats.corps.mobilite
+ if (!mobilite) return null
+
+ return CelestopolRoll.prompt({
+ actorId: this.parent.id,
+ actorName: this.parent.name,
+ actorImage: this.parent.img,
+ statId: "corps",
+ skillId: "mobilite",
+ statLabel: SYSTEM.STATS.corps.label,
+ skillLabel: SYSTEM.SKILLS.corps.mobilite.label,
+ skillValue: mobilite.value,
+ woundMalus: this.getWoundMalus(),
+ woundLevel: this.blessures.lvl,
+ rollMoonDie: this.prefs.rollMoonDie ?? false,
+ destGaugeFull: this.destin.lvl > 0,
+ fortuneValue: this.attributs.fortune.value,
+ isCombat: true,
+ isRangedDefense: true,
+ weaponType: "distance",
+ weaponName: item.name,
+ weaponDegats: "0",
})
}
}
diff --git a/module/models/npc.mjs b/module/models/npc.mjs
index 8247490..67478ee 100644
--- a/module/models/npc.mjs
+++ b/module/models/npc.mjs
@@ -41,14 +41,8 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
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({
diff --git a/packs-system/anomalies/000007.log b/packs-system/anomalies/000019.log
similarity index 100%
rename from packs-system/anomalies/000007.log
rename to packs-system/anomalies/000019.log
diff --git a/packs-system/anomalies/CURRENT b/packs-system/anomalies/CURRENT
index f7753e2..e417a51 100644
--- a/packs-system/anomalies/CURRENT
+++ b/packs-system/anomalies/CURRENT
@@ -1 +1 @@
-MANIFEST-000006
+MANIFEST-000018
diff --git a/packs-system/anomalies/LOG b/packs-system/anomalies/LOG
index a24feaa..ca546fd 100644
--- a/packs-system/anomalies/LOG
+++ b/packs-system/anomalies/LOG
@@ -1,3 +1,3 @@
-2026/03/29-17:12:00.740305 7f4bda7ed6c0 Recovering log #4
-2026/03/29-17:12:00.787211 7f4bda7ed6c0 Delete type=3 #2
-2026/03/29-17:12:00.787276 7f4bda7ed6c0 Delete type=0 #4
+2026/03/30-23:54:32.064751 7ff9c7fff6c0 Recovering log #16
+2026/03/30-23:54:32.074311 7ff9c7fff6c0 Delete type=3 #14
+2026/03/30-23:54:32.074383 7ff9c7fff6c0 Delete type=0 #16
diff --git a/packs-system/anomalies/LOG.old b/packs-system/anomalies/LOG.old
index 2014d7a..e2bb26b 100644
--- a/packs-system/anomalies/LOG.old
+++ b/packs-system/anomalies/LOG.old
@@ -1,5 +1,7 @@
-2026/03/28-09:47:34.669467 7f0018fff6c0 Delete type=3 #1
-2026/03/29-17:08:09.756858 7effca7fc6c0 Level-0 table #5: started
-2026/03/29-17:08:09.756892 7effca7fc6c0 Level-0 table #5: 0 bytes OK
-2026/03/29-17:08:09.762851 7effca7fc6c0 Delete type=0 #3
-2026/03/29-17:08:09.769416 7effca7fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+2026/03/30-09:43:32.818417 7f4bda7ed6c0 Recovering log #12
+2026/03/30-09:43:32.832361 7f4bda7ed6c0 Delete type=3 #10
+2026/03/30-09:43:32.832436 7f4bda7ed6c0 Delete type=0 #12
+2026/03/30-14:14:04.399110 7f4bd8fea6c0 Level-0 table #17: started
+2026/03/30-14:14:04.399143 7f4bd8fea6c0 Level-0 table #17: 0 bytes OK
+2026/03/30-14:14:04.436937 7f4bd8fea6c0 Delete type=0 #15
+2026/03/30-14:14:04.520163 7f4bd8fea6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
diff --git a/packs-system/anomalies/MANIFEST-000006 b/packs-system/anomalies/MANIFEST-000006
deleted file mode 100644
index 8bc3162..0000000
Binary files a/packs-system/anomalies/MANIFEST-000006 and /dev/null differ
diff --git a/packs-system/anomalies/MANIFEST-000018 b/packs-system/anomalies/MANIFEST-000018
new file mode 100644
index 0000000..1f89f0d
Binary files /dev/null and b/packs-system/anomalies/MANIFEST-000018 differ
diff --git a/packs-system/aspects/000007.log b/packs-system/aspects/000019.log
similarity index 100%
rename from packs-system/aspects/000007.log
rename to packs-system/aspects/000019.log
diff --git a/packs-system/aspects/CURRENT b/packs-system/aspects/CURRENT
index f7753e2..e417a51 100644
--- a/packs-system/aspects/CURRENT
+++ b/packs-system/aspects/CURRENT
@@ -1 +1 @@
-MANIFEST-000006
+MANIFEST-000018
diff --git a/packs-system/aspects/LOG b/packs-system/aspects/LOG
index f7becb0..11ff994 100644
--- a/packs-system/aspects/LOG
+++ b/packs-system/aspects/LOG
@@ -1,3 +1,3 @@
-2026/03/29-17:12:00.691509 7f4bd9fec6c0 Recovering log #4
-2026/03/29-17:12:00.738164 7f4bd9fec6c0 Delete type=3 #2
-2026/03/29-17:12:00.738214 7f4bd9fec6c0 Delete type=0 #4
+2026/03/30-23:54:32.051664 7ff9fd1fe6c0 Recovering log #16
+2026/03/30-23:54:32.062889 7ff9fd1fe6c0 Delete type=3 #14
+2026/03/30-23:54:32.062954 7ff9fd1fe6c0 Delete type=0 #16
diff --git a/packs-system/aspects/LOG.old b/packs-system/aspects/LOG.old
index c749628..070b37c 100644
--- a/packs-system/aspects/LOG.old
+++ b/packs-system/aspects/LOG.old
@@ -1,5 +1,7 @@
-2026/03/28-09:47:34.653497 7effcaffd6c0 Delete type=3 #1
-2026/03/29-17:08:09.762957 7effca7fc6c0 Level-0 table #5: started
-2026/03/29-17:08:09.762977 7effca7fc6c0 Level-0 table #5: 0 bytes OK
-2026/03/29-17:08:09.769218 7effca7fc6c0 Delete type=0 #3
-2026/03/29-17:08:09.769426 7effca7fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+2026/03/30-09:43:32.805788 7f4bd9fec6c0 Recovering log #12
+2026/03/30-09:43:32.816248 7f4bd9fec6c0 Delete type=3 #10
+2026/03/30-09:43:32.816303 7f4bd9fec6c0 Delete type=0 #12
+2026/03/30-14:14:04.367410 7f4bd8fea6c0 Level-0 table #17: started
+2026/03/30-14:14:04.367477 7f4bd8fea6c0 Level-0 table #17: 0 bytes OK
+2026/03/30-14:14:04.398962 7f4bd8fea6c0 Delete type=0 #15
+2026/03/30-14:14:04.520149 7f4bd8fea6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
diff --git a/packs-system/aspects/MANIFEST-000006 b/packs-system/aspects/MANIFEST-000006
deleted file mode 100644
index 8bc3162..0000000
Binary files a/packs-system/aspects/MANIFEST-000006 and /dev/null differ
diff --git a/packs-system/aspects/MANIFEST-000018 b/packs-system/aspects/MANIFEST-000018
new file mode 100644
index 0000000..1f89f0d
Binary files /dev/null and b/packs-system/aspects/MANIFEST-000018 differ
diff --git a/styles/character.less b/styles/character.less
index e09ce5e..3e0de63 100644
--- a/styles/character.less
+++ b/styles/character.less
@@ -131,39 +131,27 @@
text-align: center;
}
- // Track de niveau (cases à cocher Art Déco)
+ // Points de niveau Art Déco (remplacent les cases à cocher)
.skill-checkboxes-container {
.skill-checkboxes {
display: flex;
gap: 3px;
align-items: center;
}
- .skill-checkbox-wrapper {
- line-height: 0;
- cursor: pointer;
- .skill-level-checkbox {
- appearance: none;
- -webkit-appearance: none;
- display: inline-block;
- width: 13px;
- height: 13px;
- border: 1px solid var(--cel-border);
- border-radius: 1px;
- background: rgba(255,255,255,0.3);
- cursor: pointer;
- vertical-align: middle;
- transition: background 0.1s, border-color 0.1s;
- &:checked {
- background: var(--cel-orange);
- border-color: var(--cel-border);
- }
- &:disabled { cursor: default; }
- &:disabled:checked {
- background: var(--cel-orange);
- border-color: var(--cel-border);
- opacity: 1;
- }
+ .skill-level-dot {
+ display: inline-block;
+ width: 13px;
+ height: 13px;
+ border: 1px solid var(--cel-border);
+ border-radius: 1px;
+ background: rgba(255,255,255,0.3);
+ vertical-align: middle;
+ transition: background 0.1s, border-color 0.1s;
+ &.filled {
+ background: var(--cel-orange);
+ border-color: var(--cel-border);
}
+ &[data-action] { cursor: pointer; }
}
}
}
@@ -215,6 +203,11 @@
text-transform: uppercase;
font-size: 0.9em;
}
+ .track-title-destin {
+ cursor: help;
+ border-bottom: 1px dashed currentColor;
+ text-decoration: none;
+ }
}
.track-boxes {
@@ -228,17 +221,29 @@
display: flex;
flex-direction: column;
align-items: center;
+ justify-content: center;
gap: 2px;
+ width: 22px;
+ min-height: 22px;
+ border: 2px solid var(--cel-border);
+ border-radius: 2px;
+ background: rgba(255,255,255,0.45);
+ transition: background 0.1s, border-color 0.1s;
+
+ &.filled {
+ background: var(--cel-orange);
+ border-color: var(--cel-orange);
+ }
+
+ &[data-action] { cursor: pointer; }
- input[type="checkbox"] { .cel-box(); accent-color: var(--cel-orange); }
.box-label {
- font-size: 0.65em;
+ font-size: 0.6em;
color: var(--cel-border);
+ line-height: 1;
}
- &.checked input[type="checkbox"] {
- accent-color: var(--cel-orange);
- }
+ &.filled .box-label { color: rgba(30,10,0,0.65); }
}
}
@@ -274,6 +279,38 @@
td { padding: 4px 8px; border-bottom: 1px solid rgba(122,92,32,0.2); }
&.custom td { font-style: italic; color: #666; }
+ .faction-checkboxes-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .faction-checkboxes {
+ display: flex;
+ gap: 3px;
+ align-items: center;
+ }
+
+ .faction-dot {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ border: 1px solid var(--cel-border);
+ border-radius: 1px;
+ background: rgba(255,255,255,0.3);
+ transition: background 0.1s;
+ &.filled { background: var(--cel-orange); border-color: var(--cel-orange); }
+ &[data-action] { cursor: pointer; }
+ }
+
+ .faction-count {
+ font-size: 0.85em;
+ font-weight: bold;
+ color: var(--cel-orange);
+ min-width: 16px;
+ text-align: center;
+ }
+
.faction-value input[type="number"] {
width: 50px;
.cel-input-std();
@@ -335,6 +372,156 @@
.enriched-html { font-size: 0.9em; line-height: 1.6; }
}
+ // ── Section Expérience (onglet Biographie) ──────────────────────────────
+ .xp-section {
+ margin-bottom: 14px;
+
+ .section-header { .cel-section-header(); }
+
+ .xp-counters {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 0 8px;
+ flex-wrap: wrap;
+
+ .xp-counter {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: rgba(0,0,0,0.18);
+ border: 1px solid var(--cel-orange);
+ border-radius: 4px;
+ padding: 4px 12px;
+ min-width: 80px;
+
+ label {
+ font-size: 0.6em;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--cel-orange-light);
+ white-space: nowrap;
+ }
+
+ input[type="number"] {
+ width: 52px;
+ text-align: center;
+ .cel-input-std();
+ font-family: var(--cel-font-title);
+ font-size: 1.1em;
+ font-weight: bold;
+ color: var(--cel-orange);
+ }
+ }
+
+ .xp-depense-counter {
+ border-color: rgba(196,154,26,0.4);
+ .xp-depense-value {
+ font-family: var(--cel-font-title);
+ font-size: 1.1em;
+ font-weight: bold;
+ color: rgba(196,154,26,0.7);
+ }
+ }
+
+ .xp-btn-depenser {
+ background: var(--cel-green);
+ border: 1px solid var(--cel-orange);
+ color: var(--cel-orange);
+ font-size: 0.78em;
+ padding: 5px 12px;
+ cursor: pointer;
+ font-family: var(--cel-font-title);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ border-radius: 2px;
+ transition: background 0.15s;
+ margin-left: auto;
+ &:hover { background: var(--cel-green-light); }
+ i { margin-right: 4px; }
+ }
+ }
+
+ .xp-log-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.82em;
+ margin-bottom: 8px;
+
+ thead tr {
+ background: rgba(12,76,12,0.35);
+ th {
+ color: var(--cel-orange-light);
+ font-size: 0.75em;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 3px 6px;
+ text-align: left;
+ border-bottom: 1px solid rgba(196,154,26,0.3);
+ }
+ }
+
+ tbody tr {
+ border-bottom: 1px solid rgba(196,154,26,0.12);
+ &:nth-child(even) { background: rgba(0,0,0,0.08); }
+
+ td { padding: 3px 6px; color: var(--cel-text-dark, #3a2a0a); }
+ .xp-date { white-space: nowrap; color: var(--cel-border); font-size: 0.9em; }
+ .xp-montant { font-weight: bold; color: #c04040; white-space: nowrap; }
+
+ .xp-suppr-cell {
+ text-align: center;
+ .xp-btn-suppr {
+ background: none;
+ border: none;
+ color: rgba(180,60,60,0.6);
+ cursor: pointer;
+ font-size: 0.9em;
+ padding: 1px 4px;
+ &:hover { color: #c04040; }
+ }
+ }
+ }
+ }
+
+ // Référentiel de coûts (accordéon)
+ .xp-ref {
+ margin-top: 6px;
+ summary {
+ font-size: 0.78em;
+ color: var(--cel-orange-light);
+ cursor: pointer;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ user-select: none;
+ &:hover { color: var(--cel-orange); }
+ }
+
+ .xp-ref-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.78em;
+ margin-top: 5px;
+ opacity: 0.75;
+
+ th {
+ color: var(--cel-orange-light);
+ text-transform: uppercase;
+ font-size: 0.85em;
+ letter-spacing: 0.03em;
+ padding: 2px 6px;
+ border-bottom: 1px solid rgba(196,154,26,0.25);
+ text-align: left;
+ }
+ td {
+ padding: 2px 6px;
+ border-bottom: 1px solid rgba(196,154,26,0.1);
+ color: var(--cel-text-dark, #3a2a0a);
+ }
+ }
+ }
+ }
+
// ── Bloc Anomalie sur l'onglet Domaines ──────────────────────────────────
.anomaly-block {
border: 1px solid rgba(196,154,26,0.5);
diff --git a/styles/global.less b/styles/global.less
index 2d53039..8b6a0c4 100644
--- a/styles/global.less
+++ b/styles/global.less
@@ -277,18 +277,63 @@
&:disabled { cursor: default; opacity: 0.7; }
}
- .faction-count {
- margin-left: 8px;
- font-weight: bold;
- color: var(--cel-orange);
- min-width: 20px;
- text-align: right;
+ // ── Badge d'état de blessure intégré dans header-stats-row ─────────────────
+ .wound-status-badge {
+ // Supprime le fond/bord générique du .header-stat
+ background: transparent;
+ border-color: currentColor;
+
+ label {
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .wound-value {
+ font-family: var(--cel-font-title);
+ font-size: 0.95em;
+ font-weight: bold;
+ white-space: nowrap;
+ line-height: 1.2;
+ }
+ .wound-duration { font-weight: normal; opacity: 0.85; }
+ .wound-malus { opacity: 0.9; }
+
+ // Niveaux 1-2 : aucun malus → vert doux
+ &.wound-level-1, &.wound-level-2 {
+ color: #6abf5e;
+ background: rgba(106,191,94,0.10);
+ label { color: #6abf5e; }
+ }
+ // Niveaux 3-4 : malus -1 → ambre
+ &.wound-level-3, &.wound-level-4 {
+ color: #e8a020;
+ background: rgba(232,160,32,0.13);
+ label { color: #e8a020; }
+ }
+ // Niveaux 5-6 : malus -2 → orange vif
+ &.wound-level-5, &.wound-level-6 {
+ color: #e06020;
+ background: rgba(224,96,32,0.13);
+ label { color: #e06020; }
+ }
+ // Niveau 7 : malus -3 → rouge
+ &.wound-level-7 {
+ color: #d43030;
+ background: rgba(212,48,48,0.13);
+ label { color: #d43030; }
+ }
+ // Niveau 8 : hors combat → rouge sombre + pulsation
+ &.wound-level-8 {
+ color: #c00;
+ background: rgba(192,0,0,0.18);
+ label { color: #c00; }
+ animation: wound-pulse 1.4s ease-in-out infinite;
+ }
}
- .faction-value-input { width: 40px; margin-left: 8px; }
-
- .faction-row {
- pointer-events: auto !important;
- td { pointer-events: auto !important; }
+ @keyframes wound-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.55; }
}
+
}
diff --git a/system.json b/system.json
index a6b0a36..ab65c01 100644
--- a/system.json
+++ b/system.json
@@ -110,7 +110,7 @@
"distance": 5,
"units": "m"
},
- "primaryTokenAttribute": "resource",
+ "primaryTokenAttribute": "blessures.lvl",
"socket": true,
"background": "systems/fvtt-celestopol/assets/ui/celestopol_background.webp"
}
\ No newline at end of file
diff --git a/templates/character-biography.hbs b/templates/character-biography.hbs
index 4696644..ac48acc 100644
--- a/templates/character-biography.hbs
+++ b/templates/character-biography.hbs
@@ -1,5 +1,76 @@
+ {{!-- Section XP --}}
+
+
+
+
+
+
+ {{formInput systemFields.xp.fields.actuel value=system.xp.actuel name="system.xp.actuel"}}
+
+
+
+ {{system.xp.depense}}
+
+ {{#if isPlayMode}}
+
+ {{/if}}
+
+
+ {{!-- Log des dépenses --}}
+ {{#unless xpLogEmpty}}
+
+
+
+ | {{localize "CELESTOPOL.XP.date"}} |
+ {{localize "CELESTOPOL.XP.raison"}} |
+ {{localize "CELESTOPOL.XP.montant"}} |
+ {{#if isEditMode}} | {{/if}}
+
+
+
+ {{#each system.xp.log}}
+
+ | {{this.date}} |
+ {{this.raison}} |
+ −{{this.montant}} |
+ {{#if ../isEditMode}}
+
+
+ |
+ {{/if}}
+
+ {{/each}}
+
+
+ {{/unless}}
+
+ {{!-- Tableau de référence des coûts --}}
+
+ {{localize "CELESTOPOL.XP.refTitle"}}
+
+
+
+ | {{localize "CELESTOPOL.XP.refAmelioration"}} |
+ {{localize "CELESTOPOL.XP.refCout"}} |
+
+
+
+ | {{localize "CELESTOPOL.XP.refAugmenterSpec"}} | {{localize "CELESTOPOL.XP.refCoutNiveau"}} |
+ | {{localize "CELESTOPOL.XP.refAcquerirAspect"}} | 5 |
+ | {{localize "CELESTOPOL.XP.refAugmenterAspect"}} | 5 |
+ | {{localize "CELESTOPOL.XP.refAcquerirAttribut"}} | {{localize "CELESTOPOL.XP.refCoutAttributTotal"}} |
+
+
+
+
+
{{!-- Description / Biographie --}}
diff --git a/templates/character-blessures.hbs b/templates/character-blessures.hbs
index 6511cb8..71311cf 100644
--- a/templates/character-blessures.hbs
+++ b/templates/character-blessures.hbs
@@ -4,19 +4,14 @@
- {{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key idx|}}
-
-
-
+ {{#each (range 8) as |lvl|}}
+
+ {{lookup @root.woundLevels lvl 'malus'}}
{{/each}}
@@ -29,18 +24,13 @@
{{!-- Destin --}}
- {{#each (array "d1" "d2" "d3" "d4" "d5" "d6" "d7" "d8") as |key|}}
-
-
-
+ {{#each (range 8) as |lvl|}}
+
{{/each}}
@@ -55,15 +45,9 @@
{{localize "CELESTOPOL.Track.spleen"}}
- {{#each (array "s1" "s2" "s3" "s4" "s5" "s6" "s7" "s8") as |key|}}
-
-
-
+ {{#each (range 8) as |lvl|}}
+
{{/each}}
diff --git a/templates/character-competences.hbs b/templates/character-competences.hbs
index 0027e26..759787c 100644
--- a/templates/character-competences.hbs
+++ b/templates/character-competences.hbs
@@ -18,18 +18,13 @@
{{localize skill.label}}
-
+
{{lookup @root.system.stats statId skillId 'value'}}
{{else}}
{{localize skill.label}}
-
{{lookup (lookup @root.system.stats statId) skillId 'value'}}
+
{{lookup @root.system.stats statId skillId 'value'}}
{{/if}}
{{/each}}
diff --git a/templates/character-equipement.hbs b/templates/character-equipement.hbs
index 95bbc9c..272653e 100644
--- a/templates/character-equipement.hbs
+++ b/templates/character-equipement.hbs
@@ -18,6 +18,9 @@
{{#unless ../isEditMode}}
+ {{#if (eq item.system.type "distance")}}
+
+ {{/if}}
{{/unless}}
{{#if ../isEditMode}}
{{/if}}
diff --git a/templates/character-factions.hbs b/templates/character-factions.hbs
index 7868b95..ae725bf 100644
--- a/templates/character-factions.hbs
+++ b/templates/character-factions.hbs
@@ -13,32 +13,12 @@
|
@@ -58,32 +38,12 @@
|
@@ -100,32 +60,12 @@
|
diff --git a/templates/character-main.hbs b/templates/character-main.hbs
index 99f03c5..10a2718 100644
--- a/templates/character-main.hbs
+++ b/templates/character-main.hbs
@@ -70,6 +70,18 @@
{{/if}}
{{/each}}
+ {{#with (lookup woundLevels system.blessures.lvl) as |wound|}}
+ {{#if wound.id}}
+
+ {{/if}}
+ {{/with}}
diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs
index 9d6d720..475a689 100644
--- a/templates/chat-message.hbs
+++ b/templates/chat-message.hbs
@@ -123,7 +123,11 @@
✦✦
{{localize "CELESTOPOL.Roll.criticalSuccess"}}
{{#if isCombat}}
+ {{#if isRangedDefense}}
+
{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}
+ {{else}}
{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}
+ {{/if}}
{{else}}
{{localize "CELESTOPOL.Roll.criticalSuccessDesc"}}
{{/if}}
@@ -131,13 +135,23 @@
✦
{{localize "CELESTOPOL.Roll.success"}}
{{#if isCombat}}
+ {{#if isRangedDefense}}
+
{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}
+ {{else}}
{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}
{{/if}}
+ {{/if}}
{{else if isCriticalFailure}}
✖✖
{{localize "CELESTOPOL.Roll.criticalFailure"}}
{{#if isCombat}}
-
{{#if (eq weaponType "melee")}}{{localize "CELESTOPOL.Combat.failureHit"}}{{else}}{{localize "CELESTOPOL.Combat.distanceNoWound"}}{{/if}}
+ {{#if (eq weaponType "melee")}}
+
{{localize "CELESTOPOL.Combat.failureHit"}}
+ {{else if isRangedDefense}}
+
{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}
+ {{else}}
+
{{localize "CELESTOPOL.Combat.distanceNoWound"}}
+ {{/if}}
{{else}}
{{localize "CELESTOPOL.Roll.criticalFailureDesc"}}
{{/if}}
@@ -145,7 +159,13 @@
✖
{{localize "CELESTOPOL.Roll.failure"}}
{{#if isCombat}}
-
{{#if (eq weaponType "melee")}}{{localize "CELESTOPOL.Combat.failureHit"}}{{else}}{{localize "CELESTOPOL.Combat.distanceNoWound"}}{{/if}}
+ {{#if (eq weaponType "melee")}}
+
{{localize "CELESTOPOL.Combat.failureHit"}}
+ {{else if isRangedDefense}}
+
{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}
+ {{else}}
+
{{localize "CELESTOPOL.Combat.distanceNoWound"}}
+ {{/if}}
{{/if}}
{{/if}}
@@ -154,7 +174,7 @@
{{#if woundTaken}}
🩹
- {{#if isCombat}}{{localize "CELESTOPOL.Combat.playerWounded"}}{{else}}{{localize "CELESTOPOL.Roll.woundTaken"}}{{/if}}
+ {{#if isCombat}}{{#if isRangedDefense}}{{localize "CELESTOPOL.Combat.rangedDefensePlayerWounded"}}{{else}}{{localize "CELESTOPOL.Combat.playerWounded"}}{{/if}}{{else}}{{localize "CELESTOPOL.Roll.woundTaken"}}{{/if}}
{{/if}}
diff --git a/templates/npc-blessures.hbs b/templates/npc-blessures.hbs
index bda5e32..5d2c1f7 100644
--- a/templates/npc-blessures.hbs
+++ b/templates/npc-blessures.hbs
@@ -4,22 +4,16 @@
{{localize "CELESTOPOL.Track.blessures"}}
- {{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key|}}
-
-
-
+ {{#each (range 8) as |lvl|}}
+
+ {{lookup @root.woundLevels lvl 'malus'}}
{{/each}}
- {{#if isEditMode}}
-
- {{else}}
- {{system.blessures.lvl}}
- {{/if}}
+ {{system.blessures.lvl}}
diff --git a/templates/npc-main.hbs b/templates/npc-main.hbs
index 2cdbc65..4839985 100644
--- a/templates/npc-main.hbs
+++ b/templates/npc-main.hbs
@@ -37,6 +37,18 @@
{{localize (lookup (lookup anomalyTypes system.anomaly.type) 'label')}}
{{/if}}
+ {{#with (lookup woundLevels system.blessures.lvl) as |wound|}}
+ {{#if wound.id}}
+
+ {{/if}}
+ {{/with}}