feat: gestion de l'expérience (XP)

- Schéma xp dans CelestopolCharacter : actuel (éditable), log[] ({montant, raison, date}), depense (calculé dans prepareDerivedData)
- Bouton 'Dépenser XP' → DialogV2 (montant + raison) : décrémente actuel, logge l'entrée
- Suppression d'entrée de log avec remboursement des points (mode édition)
- Section XP en haut de l'onglet Biographie : compteurs, tableau du log, référentiel des coûts
- i18n : section CELESTOPOL.XP.* complète
- CSS : .xp-section avec compteurs, tableau de log et accordéon de référence

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-31 00:33:59 +02:00
parent 79a68ee9ab
commit 9dbd614c5a
40 changed files with 849 additions and 529 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -68,7 +68,7 @@ Hooks.once("init", () => {
// ── Sheets: unregister core, register system sheets ───────────────────── // ── Sheets: unregister core, register system sheets ─────────────────────
foundry.applications.sheets.ActorSheetV2.unregisterSheet?.("core", "Actor", { types: ["character", "npc"] }) 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, { foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, CelestopolCharacterSheet, {
types: ["character"], types: ["character"],
makeDefault: true, makeDefault: true,
@@ -80,7 +80,7 @@ Hooks.once("init", () => {
label: "CELESTOPOL.Sheet.npc", 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, { foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolAnomalySheet, {
types: ["anomaly"], types: ["anomaly"],
makeDefault: true, makeDefault: true,
@@ -130,6 +130,7 @@ Hooks.once("ready", () => {
// Migration : supprime les items de types obsolètes (ex: "attribute") // Migration : supprime les items de types obsolètes (ex: "attribute")
if (game.user.isGM) { if (game.user.isGM) {
_migrateObsoleteItems() _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 ─────────────────────────────────────────────────── */ /* ─── Handlebars helpers ─────────────────────────────────────────────────── */
function _registerHandlebarsHelpers() { function _registerHandlebarsHelpers() {
@@ -183,6 +246,9 @@ function _registerHandlebarsHelpers() {
// Helper : build array from args (Handlebars doesn't have arrays natively) // Helper : build array from args (Handlebars doesn't have arrays natively)
Handlebars.registerHelper("array", (...args) => args.slice(0, -1)) 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 // Helper : nested object lookup with dot path or multiple keys
Handlebars.registerHelper("lookup", (obj, ...args) => { Handlebars.registerHelper("lookup", (obj, ...args) => {
const options = args.pop() // last arg is Handlebars options hash const options = args.pop() // last arg is Handlebars options hash

View File

@@ -95,29 +95,40 @@
"destin": "Destin", "destin": "Destin",
"spleen": "Spleen", "spleen": "Spleen",
"level": "Niveau", "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": { "Wound": {
"none": "Aucune blessure", "none": "Aucune blessure",
"anodin": "Anodin", "anodin": "Anodin",
"derisoire": "Dérisoire", "derisoire": "Dérisoire",
"negligeable": "Négligeable", "negligeable": "Négligeable",
"superficiel": "Superficiel", "superficiel": "Superficiel",
"leger": "Léger", "leger": "Léger",
"modere": "Modéré", "modere": "Modéré",
"grave": "Grave", "grave": "Grave",
"dramatique": "Dramatique (hors combat)" "dramatique": "Dramatique (hors combat)",
"duration1min": "1 min",
"duration10min": "10 min",
"duration30min": "30 min",
"duration1jour": "1 journée",
"status": "État : "
}, },
"Combat": { "Combat": {
"attack": "Attaquer", "attack": "Attaquer",
"corpsPnj": "Corps du PNJ", "corpsPnj": "Corps du PNJ",
"tie": "ÉGALITÉ", "tie": "ÉGALITÉ",
"tieDesc": "Personne n'est blessé", "tieDesc": "Personne n'est blessé",
"successHit": "PNJ touché — 1 blessure", "successHit": "PNJ touché — 1 blessure",
"failureHit": "Joueur touché — 1 blessure (mêlée)", "failureHit": "Joueur touché — 1 blessure (mêlée)",
"distanceNoWound": "Raté — pas de riposte", "distanceNoWound": "Raté — pas de riposte",
"weaponDamage": "dégâts supplémentaires", "weaponDamage": "dégâts supplémentaires",
"playerWounded": "Blessure infligée au joueur (mêlée)" "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": { "Tab": {
"main": "Principal", "main": "Principal",
@@ -294,6 +305,30 @@
}, },
"Aspect": { "Aspect": {
"valeur": "Valeur" "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"
} }
} }
} }

View File

@@ -1,3 +1,5 @@
import { SYSTEM } from "../../config/system.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api const { HandlebarsApplicationMixin } = foundry.applications.api
export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) { export default class CelestopolActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
@@ -18,11 +20,15 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
window: { resizable: true }, window: { resizable: true },
dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }], dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }],
actions: { actions: {
editImage: CelestopolActorSheet.#onEditImage, editImage: CelestopolActorSheet.#onEditImage,
toggleSheet: CelestopolActorSheet.#onToggleSheet, toggleSheet: CelestopolActorSheet.#onToggleSheet,
edit: CelestopolActorSheet.#onItemEdit, edit: CelestopolActorSheet.#onItemEdit,
delete: CelestopolActorSheet.#onItemDelete, delete: CelestopolActorSheet.#onItemDelete,
attack: CelestopolActorSheet.#onAttack, 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, isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode, isPlayMode: this.isPlayMode,
isEditable: this.isEditable, isEditable: this.isEditable,
woundLevels: SYSTEM.WOUND_LEVELS,
} }
} }
@@ -51,28 +58,9 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
this.element.querySelectorAll(".rollable").forEach(el => { this.element.querySelectorAll(".rollable").forEach(el => {
el.addEventListener("click", this._onRoll.bind(this)) 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) { async _onRoll(event) {
// Don't roll if clicking on a checkbox
if (event.target.classList.contains('skill-level-checkbox')) {
return
}
if (!this.isPlayMode) return if (!this.isPlayMode) return
const el = event.currentTarget const el = event.currentTarget
const statId = el.dataset.statId const statId = el.dataset.statId
@@ -158,132 +146,39 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
await this.document.system.rollAttack(itemId) await this.document.system.rollAttack(itemId)
} }
/** static async #onRangedDefense(_event, target) {
* Setup sequential checkbox logic for wound/destin/spleen tracks const itemId = target.getAttribute("data-item-id")
* Only allows checking the next checkbox in sequence if (!itemId) return
*/ await this.document.system.rollRangedDefense(itemId)
_setupSequentialCheckboxes() {
this.element.querySelectorAll('.wound-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (event) => {
this._handleSequentialCheckboxChange(event)
})
})
} }
/** /** Met à jour une jauge de piste (blessures/destin/spleen) par clic sur une case. */
* Handle sequential checkbox change logic static #onTrackBox(_event, target) {
* @param {Event} event - The change event if (!this.isEditable) return
*/ const path = target.dataset.path
_handleSequentialCheckboxChange(event) { const index = parseInt(target.dataset.index)
const checkbox = event.target const current = foundry.utils.getProperty(this.document, path) ?? 0
if (!checkbox.classList.contains('wound-checkbox') || checkbox.disabled) return const newValue = (index <= current) ? index - 1 : index
this.document.update({ [path]: Math.max(0, newValue) })
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 la valeur d'un domaine par clic sur un point de niveau. */
* Update visual state of track boxes based on checkbox states static #onSkillLevel(_event, target) {
*/ if (!this.isEditable) return
_updateTrackVisualState() { const { statId, skillId } = target.dataset
this.element.querySelectorAll('.track-box').forEach(box => { const index = parseInt(target.dataset.index)
const checkbox = box.querySelector('.wound-checkbox') const current = this.document.system.stats[statId]?.[skillId]?.value ?? 0
if (checkbox) { const newValue = (index <= current) ? index - 1 : index
if (checkbox.checked) { this.document.update({ [`system.stats.${statId}.${skillId}.value`]: Math.max(0, newValue) })
box.classList.add('checked')
} else {
box.classList.remove('checked')
}
}
})
} }
/** /** Met à jour le score d'une faction par clic sur un point. */
* Setup sequential checkbox logic for faction tracks static #onFactionLevel(_event, target) {
*/ if (!this.isEditable) return
_setupFactionCheckboxes() { const factionId = target.dataset.faction
this.element.querySelectorAll('.faction-checkbox').forEach(checkbox => { const index = parseInt(target.dataset.index)
checkbox.addEventListener('change', (event) => { const current = this.document.system.factions[factionId]?.value ?? 0
this._handleFactionCheckboxChange(event) const newValue = (index <= current) ? index - 1 : index
}) this.document.update({ [`system.factions.${factionId}.value`]: Math.max(0, newValue) })
})
}
/**
* 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
}
} }
} }

View File

@@ -15,6 +15,8 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
createArmure: CelestopolCharacterSheet.#onCreateArmure, createArmure: CelestopolCharacterSheet.#onCreateArmure,
useAnomaly: CelestopolCharacterSheet.#onUseAnomaly, useAnomaly: CelestopolCharacterSheet.#onUseAnomaly,
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses, resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
depenseXp: CelestopolCharacterSheet.#onDepenseXp,
supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
}, },
} }
@@ -95,6 +97,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
case "biography": case "biography":
context.tab = context.tabs.biography context.tab = context.tabs.biography
context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.description, { async: true }) doc.system.description, { async: true })
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML( context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
@@ -111,36 +114,36 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
return context return context
} }
static #onCreateAnomaly() { static async #onCreateAnomaly() {
if (this.document.itemTypes.anomaly.length > 0) { if (this.document.itemTypes.anomaly.length > 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly")) ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
return return
} }
this.document.createEmbeddedDocuments("Item", [{ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly", name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
}]) }])
} }
static #onCreateAspect() { static async #onCreateAspect() {
this.document.createEmbeddedDocuments("Item", [{ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect", name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
}]) }])
} }
static #onCreateEquipment() { static async #onCreateEquipment() {
this.document.createEmbeddedDocuments("Item", [{ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.equipment"), type: "equipment", name: game.i18n.localize("TYPES.Item.equipment"), type: "equipment",
}]) }])
} }
static #onCreateWeapon() { static async #onCreateWeapon() {
this.document.createEmbeddedDocuments("Item", [{ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon", name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon",
}]) }])
} }
static #onCreateArmure() { static async #onCreateArmure() {
this.document.createEmbeddedDocuments("Item", [{ await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.armure"), type: "armure", name: game.i18n.localize("TYPES.Item.armure"), type: "armure",
}]) }])
} }
@@ -163,4 +166,73 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
if (!anomaly) return if (!anomaly) return
await anomaly.update({ "system.usesRemaining": anomaly.system.level }) 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 = `
<form class="cel-dialog-form">
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.XP.montant")}</label>
<input type="number" name="montant" value="1" min="1" max="${currentXp}" autofocus />
</div>
<div class="form-group">
<label>${i18n.localize("CELESTOPOL.XP.raison")}</label>
<input type="text" name="raison" placeholder="${i18n.localize("CELESTOPOL.XP.raisonPlaceholder")}" />
</div>
<p class="xp-dialog-hint">${i18n.format("CELESTOPOL.XP.disponible", { n: currentXp })}</p>
</form>`
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,
})
}
} }

View File

@@ -90,15 +90,15 @@ export const FACTIONS = {
/** Niveaux de blessures avec leur malus associé. */ /** Niveaux de blessures avec leur malus associé. */
export const WOUND_LEVELS = [ export const WOUND_LEVELS = [
{ id: 0, label: "CELESTOPOL.Wound.none", malus: 0 }, { id: 0, label: "CELESTOPOL.Wound.none", malus: 0, duration: "" },
{ id: 1, label: "CELESTOPOL.Wound.anodin", malus: 0 }, { id: 1, label: "CELESTOPOL.Wound.anodin", malus: 0, duration: "CELESTOPOL.Wound.duration1min" },
{ id: 2, label: "CELESTOPOL.Wound.derisoire", malus: 0 }, { id: 2, label: "CELESTOPOL.Wound.negligeable", malus: 0, duration: "CELESTOPOL.Wound.duration1min" },
{ id: 3, label: "CELESTOPOL.Wound.negligeable", malus: -1 }, { id: 3, label: "CELESTOPOL.Wound.derisoire", malus: -1, duration: "CELESTOPOL.Wound.duration10min" },
{ id: 4, label: "CELESTOPOL.Wound.superficiel", malus: -1 }, { id: 4, label: "CELESTOPOL.Wound.superficiel", malus: -1, duration: "CELESTOPOL.Wound.duration10min" },
{ id: 5, label: "CELESTOPOL.Wound.leger", malus: -2 }, { id: 5, label: "CELESTOPOL.Wound.leger", malus: -2, duration: "CELESTOPOL.Wound.duration30min" },
{ id: 6, label: "CELESTOPOL.Wound.modere", malus: -2 }, { id: 6, label: "CELESTOPOL.Wound.modere", malus: -2, duration: "CELESTOPOL.Wound.duration30min" },
{ id: 7, label: "CELESTOPOL.Wound.grave", malus: -3 }, { id: 7, label: "CELESTOPOL.Wound.grave", malus: -3, duration: "CELESTOPOL.Wound.duration1jour" },
{ id: 8, label: "CELESTOPOL.Wound.dramatique", malus: -999 }, { id: 8, label: "CELESTOPOL.Wound.dramatique", malus: -999, duration: "" },
] ]
/** Seuils de difficulté pour les jets de dés. */ /** Seuils de difficulté pour les jets de dés. */

View File

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

View File

@@ -1,6 +1 @@
export default class CelestopolChatMessage extends ChatMessage { export default class CelestopolChatMessage extends ChatMessage {}
/** @override */
async renderHTML(options = {}) {
return super.renderHTML(options)
}
}

View File

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

View File

@@ -42,6 +42,7 @@ export class CelestopolRoll extends Roll {
const fortuneValue = options.fortuneValue ?? 0 const fortuneValue = options.fortuneValue ?? 0
const isResistance = options.isResistance ?? false const isResistance = options.isResistance ?? false
const isCombat = options.isCombat ?? false const isCombat = options.isCombat ?? false
const isRangedDefense = options.isRangedDefense ?? false
const weaponType = options.weaponType ?? "melee" const weaponType = options.weaponType ?? "melee"
const weaponName = options.weaponName ?? null const weaponName = options.weaponName ?? null
const weaponDegats = options.weaponDegats ?? "0" const weaponDegats = options.weaponDegats ?? "0"
@@ -72,6 +73,7 @@ export class CelestopolRoll extends Roll {
defaultRollMoonDie: options.rollMoonDie ?? false, defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance, isResistance,
isCombat, isCombat,
isRangedDefense,
weaponType, weaponType,
weaponName, weaponName,
weaponDegats, weaponDegats,
@@ -221,6 +223,7 @@ export class CelestopolRoll extends Roll {
autoSuccess, autoSuccess,
isResistance, isResistance,
isCombat, isCombat,
isRangedDefense,
weaponType, weaponType,
weaponName, weaponName,
weaponDegats, weaponDegats,
@@ -243,66 +246,49 @@ export class CelestopolRoll extends Roll {
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure // Test de résistance échoué → cocher automatiquement la prochaine case de blessure
const actor = game.actors.get(options.actorId) const actor = game.actors.get(options.actorId)
if (isResistance && actor && roll.options.resultType === "failure") { if (isResistance && actor && roll.options.resultType === "failure") {
const wounds = actor.system.blessures const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked) if (nextLvl <= 8) {
if (nextIdx) { await actor.update({ "system.blessures.lvl": nextLvl })
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true }) roll.options.woundTaken = nextLvl
roll.options.woundTaken = nextIdx
} }
} }
// Combat mêlée échoué → joueur prend une blessure // Mêlée échouée OU défense à distance échouée → joueur prend une blessure
if (isCombat && weaponType === "melee" && actor && roll.options.resultType === "failure") { if (isCombat && (weaponType === "melee" || isRangedDefense) && actor && roll.options.resultType === "failure") {
const wounds = actor.system.blessures const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked) if (nextLvl <= 8) {
if (nextIdx) { await actor.update({ "system.blessures.lvl": nextLvl })
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true }) roll.options.woundTaken = nextLvl
roll.options.woundTaken = nextIdx
} }
} }
await roll.toMessage({}, { rollMode: rollData.rollMode }) await roll.toMessage({}, { rollMode: rollData.rollMode })
// Destin utilisé → vider la jauge (reset à 0) // Batching de toutes les mises à jour de l'acteur en un seul appel réseau
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
if (actor) { if (actor) {
await actor.update({ const updateData = {}
"system.prefs.rollMoonDie": rollData.rollMoonDie,
"system.prefs.difficulty": difficulty, 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 return roll
@@ -421,6 +407,7 @@ export class CelestopolRoll extends Roll {
weaponName: this.options.weaponName ?? null, weaponName: this.options.weaponName ?? null,
weaponDegats: this.options.weaponDegats ?? null, weaponDegats: this.options.weaponDegats ?? null,
weaponType: this.options.weaponType ?? null, weaponType: this.options.weaponType ?? null,
isRangedDefense: this.options.isRangedDefense ?? false,
woundTaken: this.options.woundTaken ?? null, woundTaken: this.options.woundTaken ?? null,
// Dé de lune // Dé de lune
hasMoonDie: moonDieResult !== null, hasMoonDie: moonDieResult !== null,

View File

@@ -21,18 +21,10 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), 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({ const skillField = (label) => new fields.SchemaField({
label: new fields.StringField({ required: true, initial: label }), label: new fields.StringField({ required: true, initial: label }),
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), 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) => { const statField = (statId) => {
@@ -55,32 +47,19 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
esprit: statField("esprit"), esprit: statField("esprit"),
}) })
// Blessures (8 cases) // Blessures — niveau entier direct (0 = aucune, 8 = fatale)
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({ schema.blessures = new fields.SchemaField({
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), 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) // Destin — jauge entière directe
const destField = () => new fields.SchemaField({
checked: new fields.BooleanField({ required: true, initial: false }),
})
schema.destin = new fields.SchemaField({ schema.destin = new fields.SchemaField({
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), 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({ schema.spleen = new fields.SchemaField({
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), 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) // Attributs de personnage (Entregent, Fortune, Rêve, Vision)
@@ -95,18 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
vision: persoAttrField(), vision: persoAttrField(),
}) })
// Factions - 9 checkboxes per faction (like wound tracks) // Factions — score entier direct (0-9)
const factionField = () => new fields.SchemaField({ const factionField = () => new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0 }), value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 9 }),
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 }),
}) })
schema.factions = new fields.SchemaField({ schema.factions = new fields.SchemaField({
pinkerton: factionField(), pinkerton: factionField(),
@@ -133,6 +103,16 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }), 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 // Description & notes
schema.description = new fields.HTMLField({ required: true, textSearch: true }) schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = 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() { prepareDerivedData() {
super.prepareDerivedData() super.prepareDerivedData()
// Calcul automatique de la valeur de chaque domaine = nombre de cases cochées // Résistance par stat = +2 par domaine atteignant son seuil de spécialisation
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
for (const [statId, statData] of Object.entries(this.stats)) { for (const [statId, statData] of Object.entries(this.stats)) {
let res = 0 let res = 0
for (const [skillId, skill] of Object.entries(statData)) { for (const [skillId, skill] of Object.entries(statData)) {
@@ -175,19 +147,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
statData.res = res statData.res = res
} }
// Calcul automatique de la valeur de chaque faction = nombre de cases cochées // Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur)
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]
this.initiative = 4 + (this.stats.corps.mobilite?.value ?? 0) + (this.stats.coeur.inspiration?.value ?? 0) 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] const statData = this.stats[statId]
if (!statData) return null 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). * Lance une attaque avec une arme.
* Mêlée : échec → blessure joueur auto-cochée. * Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur.
* Distance : échec → simple raté, pas de blessure joueur. * Distance : test Échauffourée vs Corps PNJ ; échec → pas de blessure joueur.
* Égalité (marge=0) → personne n'est blessé. * Égalité (marge=0) → personne n'est blessé.
* @param {string} itemId - Id de l'item arme * @param {string} itemId - Id de l'item arme
*/ */
@@ -255,23 +238,60 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
if (!echauffouree) return null if (!echauffouree) return null
return CelestopolRoll.prompt({ return CelestopolRoll.prompt({
actorId: this.parent.id, actorId: this.parent.id,
actorName: this.parent.name, actorName: this.parent.name,
actorImage: this.parent.img, actorImage: this.parent.img,
statId: "corps", statId: "corps",
skillId: "echauffouree", skillId: "echauffouree",
statLabel: SYSTEM.STATS.corps.label, statLabel: SYSTEM.STATS.corps.label,
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label, skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
skillValue: echauffouree.value, skillValue: echauffouree.value,
woundMalus: this.getWoundMalus(), woundMalus: this.getWoundMalus(),
woundLevel: this.blessures.lvl, woundLevel: this.blessures.lvl,
rollMoonDie: this.prefs.rollMoonDie ?? false, rollMoonDie: this.prefs.rollMoonDie ?? false,
destGaugeFull: this.destin.lvl > 0, destGaugeFull: this.destin.lvl > 0,
fortuneValue: this.attributs.fortune.value, fortuneValue: this.attributs.fortune.value,
isCombat: true, isCombat: true,
weaponType: item.system.type, isRangedDefense: false,
weaponName: item.name, weaponType: item.system.type,
weaponDegats: item.system.degats, 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",
}) })
} }
} }

View File

@@ -41,14 +41,8 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
esprit: statField("esprit"), 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({ schema.blessures = new fields.SchemaField({
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), 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({ schema.prefs = new fields.SchemaField({

View File

@@ -1 +1 @@
MANIFEST-000006 MANIFEST-000018

View File

@@ -1,3 +1,3 @@
2026/03/29-17:12:00.740305 7f4bda7ed6c0 Recovering log #4 2026/03/30-23:54:32.064751 7ff9c7fff6c0 Recovering log #16
2026/03/29-17:12:00.787211 7f4bda7ed6c0 Delete type=3 #2 2026/03/30-23:54:32.074311 7ff9c7fff6c0 Delete type=3 #14
2026/03/29-17:12:00.787276 7f4bda7ed6c0 Delete type=0 #4 2026/03/30-23:54:32.074383 7ff9c7fff6c0 Delete type=0 #16

View File

@@ -1,5 +1,7 @@
2026/03/28-09:47:34.669467 7f0018fff6c0 Delete type=3 #1 2026/03/30-09:43:32.818417 7f4bda7ed6c0 Recovering log #12
2026/03/29-17:08:09.756858 7effca7fc6c0 Level-0 table #5: started 2026/03/30-09:43:32.832361 7f4bda7ed6c0 Delete type=3 #10
2026/03/29-17:08:09.756892 7effca7fc6c0 Level-0 table #5: 0 bytes OK 2026/03/30-09:43:32.832436 7f4bda7ed6c0 Delete type=0 #12
2026/03/29-17:08:09.762851 7effca7fc6c0 Delete type=0 #3 2026/03/30-14:14:04.399110 7f4bd8fea6c0 Level-0 table #17: started
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-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)

Binary file not shown.

View File

@@ -1 +1 @@
MANIFEST-000006 MANIFEST-000018

View File

@@ -1,3 +1,3 @@
2026/03/29-17:12:00.691509 7f4bd9fec6c0 Recovering log #4 2026/03/30-23:54:32.051664 7ff9fd1fe6c0 Recovering log #16
2026/03/29-17:12:00.738164 7f4bd9fec6c0 Delete type=3 #2 2026/03/30-23:54:32.062889 7ff9fd1fe6c0 Delete type=3 #14
2026/03/29-17:12:00.738214 7f4bd9fec6c0 Delete type=0 #4 2026/03/30-23:54:32.062954 7ff9fd1fe6c0 Delete type=0 #16

View File

@@ -1,5 +1,7 @@
2026/03/28-09:47:34.653497 7effcaffd6c0 Delete type=3 #1 2026/03/30-09:43:32.805788 7f4bd9fec6c0 Recovering log #12
2026/03/29-17:08:09.762957 7effca7fc6c0 Level-0 table #5: started 2026/03/30-09:43:32.816248 7f4bd9fec6c0 Delete type=3 #10
2026/03/29-17:08:09.762977 7effca7fc6c0 Level-0 table #5: 0 bytes OK 2026/03/30-09:43:32.816303 7f4bd9fec6c0 Delete type=0 #12
2026/03/29-17:08:09.769218 7effca7fc6c0 Delete type=0 #3 2026/03/30-14:14:04.367410 7f4bd8fea6c0 Level-0 table #17: started
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-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)

Binary file not shown.

Binary file not shown.

View File

@@ -131,39 +131,27 @@
text-align: center; 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-container {
.skill-checkboxes { .skill-checkboxes {
display: flex; display: flex;
gap: 3px; gap: 3px;
align-items: center; align-items: center;
} }
.skill-checkbox-wrapper { .skill-level-dot {
line-height: 0; display: inline-block;
cursor: pointer; width: 13px;
.skill-level-checkbox { height: 13px;
appearance: none; border: 1px solid var(--cel-border);
-webkit-appearance: none; border-radius: 1px;
display: inline-block; background: rgba(255,255,255,0.3);
width: 13px; vertical-align: middle;
height: 13px; transition: background 0.1s, border-color 0.1s;
border: 1px solid var(--cel-border); &.filled {
border-radius: 1px; background: var(--cel-orange);
background: rgba(255,255,255,0.3); border-color: var(--cel-border);
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;
}
} }
&[data-action] { cursor: pointer; }
} }
} }
} }
@@ -215,6 +203,11 @@
text-transform: uppercase; text-transform: uppercase;
font-size: 0.9em; font-size: 0.9em;
} }
.track-title-destin {
cursor: help;
border-bottom: 1px dashed currentColor;
text-decoration: none;
}
} }
.track-boxes { .track-boxes {
@@ -228,17 +221,29 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
gap: 2px; 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 { .box-label {
font-size: 0.65em; font-size: 0.6em;
color: var(--cel-border); color: var(--cel-border);
line-height: 1;
} }
&.checked input[type="checkbox"] { &.filled .box-label { color: rgba(30,10,0,0.65); }
accent-color: var(--cel-orange);
}
} }
} }
@@ -274,6 +279,38 @@
td { padding: 4px 8px; border-bottom: 1px solid rgba(122,92,32,0.2); } td { padding: 4px 8px; border-bottom: 1px solid rgba(122,92,32,0.2); }
&.custom td { font-style: italic; color: #666; } &.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"] { .faction-value input[type="number"] {
width: 50px; width: 50px;
.cel-input-std(); .cel-input-std();
@@ -335,6 +372,156 @@
.enriched-html { font-size: 0.9em; line-height: 1.6; } .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 ────────────────────────────────── // ── Bloc Anomalie sur l'onglet Domaines ──────────────────────────────────
.anomaly-block { .anomaly-block {
border: 1px solid rgba(196,154,26,0.5); border: 1px solid rgba(196,154,26,0.5);

View File

@@ -277,18 +277,63 @@
&:disabled { cursor: default; opacity: 0.7; } &:disabled { cursor: default; opacity: 0.7; }
} }
.faction-count { // ── Badge d'état de blessure intégré dans header-stats-row ─────────────────
margin-left: 8px; .wound-status-badge {
font-weight: bold; // Supprime le fond/bord générique du .header-stat
color: var(--cel-orange); background: transparent;
min-width: 20px; border-color: currentColor;
text-align: right;
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; } @keyframes wound-pulse {
0%, 100% { opacity: 1; }
.faction-row { 50% { opacity: 0.55; }
pointer-events: auto !important;
td { pointer-events: auto !important; }
} }
} }

View File

@@ -110,7 +110,7 @@
"distance": 5, "distance": 5,
"units": "m" "units": "m"
}, },
"primaryTokenAttribute": "resource", "primaryTokenAttribute": "blessures.lvl",
"socket": true, "socket": true,
"background": "systems/fvtt-celestopol/assets/ui/celestopol_background.webp" "background": "systems/fvtt-celestopol/assets/ui/celestopol_background.webp"
} }

View File

@@ -1,5 +1,76 @@
<div class="tab biography {{tab.cssClass}}" data-group="sheet" data-tab="biography"> <div class="tab biography {{tab.cssClass}}" data-group="sheet" data-tab="biography">
{{!-- Section XP --}}
<div class="xp-section">
<div class="section-header">{{localize "CELESTOPOL.XP.title"}}</div>
<div class="xp-counters">
<div class="xp-counter">
<label>{{localize "CELESTOPOL.XP.actuel"}}</label>
{{formInput systemFields.xp.fields.actuel value=system.xp.actuel name="system.xp.actuel"}}
</div>
<div class="xp-counter xp-depense-counter">
<label>{{localize "CELESTOPOL.XP.depense"}}</label>
<span class="xp-depense-value">{{system.xp.depense}}</span>
</div>
{{#if isPlayMode}}
<button type="button" class="xp-btn-depenser" data-action="depenseXp">
<i class="fa-solid fa-coins"></i> {{localize "CELESTOPOL.XP.depenser"}}
</button>
{{/if}}
</div>
{{!-- Log des dépenses --}}
{{#unless xpLogEmpty}}
<table class="xp-log-table">
<thead>
<tr>
<th>{{localize "CELESTOPOL.XP.date"}}</th>
<th>{{localize "CELESTOPOL.XP.raison"}}</th>
<th>{{localize "CELESTOPOL.XP.montant"}}</th>
{{#if isEditMode}}<th></th>{{/if}}
</tr>
</thead>
<tbody>
{{#each system.xp.log}}
<tr>
<td class="xp-date">{{this.date}}</td>
<td class="xp-raison">{{this.raison}}</td>
<td class="xp-montant">{{this.montant}}</td>
{{#if ../isEditMode}}
<td class="xp-suppr-cell">
<button type="button" class="xp-btn-suppr" data-action="supprimerXpLog"
data-idx="{{@index}}" title="{{localize 'CELESTOPOL.XP.supprimer'}}">
<i class="fa-solid fa-trash"></i>
</button>
</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
{{/unless}}
{{!-- Tableau de référence des coûts --}}
<details class="xp-ref">
<summary>{{localize "CELESTOPOL.XP.refTitle"}}</summary>
<table class="xp-ref-table">
<thead>
<tr>
<th>{{localize "CELESTOPOL.XP.refAmelioration"}}</th>
<th>{{localize "CELESTOPOL.XP.refCout"}}</th>
</tr>
</thead>
<tbody>
<tr><td>{{localize "CELESTOPOL.XP.refAugmenterSpec"}}</td><td>{{localize "CELESTOPOL.XP.refCoutNiveau"}}</td></tr>
<tr><td>{{localize "CELESTOPOL.XP.refAcquerirAspect"}}</td><td>5</td></tr>
<tr><td>{{localize "CELESTOPOL.XP.refAugmenterAspect"}}</td><td>5</td></tr>
<tr><td>{{localize "CELESTOPOL.XP.refAcquerirAttribut"}}</td><td>{{localize "CELESTOPOL.XP.refCoutAttributTotal"}}</td></tr>
</tbody>
</table>
</details>
</div>
{{!-- Description / Biographie --}} {{!-- Description / Biographie --}}
<div class="biography-section"> <div class="biography-section">
<div class="section-header">{{localize "CELESTOPOL.Actor.description"}}</div> <div class="section-header">{{localize "CELESTOPOL.Actor.description"}}</div>

View File

@@ -4,19 +4,14 @@
<div class="track-header"> <div class="track-header">
<span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span> <span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span>
<span class="wound-malus">{{localize "CELESTOPOL.Track.currentMalus"}} : <span class="wound-malus">{{localize "CELESTOPOL.Track.currentMalus"}} :
<strong>{{system.blessures.lvl}}</strong> <strong>{{lookup @root.woundLevels system.blessures.lvl 'malus'}}</strong>
</span> </span>
</div> </div>
<div class="track-boxes"> <div class="track-boxes">
{{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key idx|}} {{#each (range 8) as |lvl|}}
<div class="track-box {{#if (lookup ../system.blessures key 'checked')}}checked{{/if}}"> <div class="track-box {{#if (lte lvl ../system.blessures.lvl)}}filled{{/if}}"
<input type="checkbox" name="system.blessures.{{key}}.checked" {{#if ../isEditable}}data-action="trackBox" data-path="system.blessures.lvl" data-index="{{lvl}}"{{/if}}>
{{#if (lookup ../system.blessures key 'checked')}}checked{{/if}} <span class="box-label">{{lookup @root.woundLevels lvl 'malus'}}</span>
{{#unless ../isEditable}}disabled{{/unless}}
class="wound-checkbox"
data-track="blessures"
data-index="{{idx}}">
<label class="box-label">{{lookup ../system.blessures key 'malus'}}</label>
</div> </div>
{{/each}} {{/each}}
</div> </div>
@@ -29,18 +24,13 @@
{{!-- Destin --}} {{!-- Destin --}}
<section class="track-section"> <section class="track-section">
<div class="track-header"> <div class="track-header">
<span class="track-title">{{localize "CELESTOPOL.Track.destin"}}</span> <span class="track-title track-title-destin"
title="{{localize 'CELESTOPOL.Track.destinTooltip'}}">{{localize "CELESTOPOL.Track.destin"}}</span>
</div> </div>
<div class="track-boxes destin-boxes"> <div class="track-boxes destin-boxes">
{{#each (array "d1" "d2" "d3" "d4" "d5" "d6" "d7" "d8") as |key|}} {{#each (range 8) as |lvl|}}
<div class="track-box destiny {{#if (lookup ../system.destin key 'checked')}}checked{{/if}}"> <div class="track-box destiny {{#if (lte lvl ../system.destin.lvl)}}filled{{/if}}"
<input type="checkbox" name="system.destin.{{key}}.checked" {{#if ../isEditable}}data-action="trackBox" data-path="system.destin.lvl" data-index="{{lvl}}"{{/if}}></div>
{{#if (lookup ../system.destin key 'checked')}}checked{{/if}}
{{#unless ../isEditable}}disabled{{/unless}}
class="wound-checkbox"
data-track="destin"
data-index="{{@index}}">
</div>
{{/each}} {{/each}}
</div> </div>
<div class="track-level"> <div class="track-level">
@@ -55,15 +45,9 @@
<span class="track-title">{{localize "CELESTOPOL.Track.spleen"}}</span> <span class="track-title">{{localize "CELESTOPOL.Track.spleen"}}</span>
</div> </div>
<div class="track-boxes spleen-boxes"> <div class="track-boxes spleen-boxes">
{{#each (array "s1" "s2" "s3" "s4" "s5" "s6" "s7" "s8") as |key|}} {{#each (range 8) as |lvl|}}
<div class="track-box spleen {{#if (lookup ../system.spleen key 'checked')}}checked{{/if}}"> <div class="track-box spleen {{#if (lte lvl ../system.spleen.lvl)}}filled{{/if}}"
<input type="checkbox" name="system.spleen.{{key}}.checked" {{#if ../isEditable}}data-action="trackBox" data-path="system.spleen.lvl" data-index="{{lvl}}"{{/if}}></div>
{{#if (lookup ../system.spleen key 'checked')}}checked{{/if}}
{{#unless ../isEditable}}disabled{{/unless}}
class="wound-checkbox"
data-track="spleen"
data-index="{{@index}}">
</div>
{{/each}} {{/each}}
</div> </div>
<div class="track-level"> <div class="track-level">

View File

@@ -18,18 +18,13 @@
<span class="skill-name">{{localize skill.label}}</span> <span class="skill-name">{{localize skill.label}}</span>
<div class="skill-checkboxes-container"> <div class="skill-checkboxes-container">
<div class="skill-checkboxes"> <div class="skill-checkboxes">
{{#each (array 1 2 3 4 5 6 7 8) as |level|}} {{#each (range 8) as |lvl|}}
<label class="skill-checkbox-wrapper"> <span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}}"
<input type="checkbox" name="system.stats.{{statId}}.{{skillId}}.level{{level}}" data-action="skillLevel" data-stat-id="{{statId}}" data-skill-id="{{skillId}}" data-index="{{lvl}}"></span>
{{#if (lookup (lookup (lookup @root.system.stats statId) skillId) (concat 'level' level))}}checked{{/if}}
class="skill-level-checkbox">
</label>
{{/each}} {{/each}}
</div> </div>
</div> </div>
<input type="number" name="system.stats.{{statId}}.{{skillId}}.value" <span class="skill-value">{{lookup @root.system.stats statId skillId 'value'}}</span>
value="{{lookup (lookup @root.system.stats statId) skillId 'value'}}"
min="0" max="8" class="skill-value-input">
</div> </div>
{{else}} {{else}}
<div class="skill-row rollable" data-stat-id="{{statId}}" data-skill-id="{{skillId}}" <div class="skill-row rollable" data-stat-id="{{statId}}" data-skill-id="{{skillId}}"
@@ -37,16 +32,12 @@
<span class="skill-name">{{localize skill.label}}</span> <span class="skill-name">{{localize skill.label}}</span>
<div class="skill-checkboxes-container"> <div class="skill-checkboxes-container">
<div class="skill-checkboxes"> <div class="skill-checkboxes">
{{#each (array 1 2 3 4 5 6 7 8) as |level|}} {{#each (range 8) as |lvl|}}
<label class="skill-checkbox-wrapper"> <span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}}"></span>
<input type="checkbox"
{{#if (lookup (lookup (lookup @root.system.stats statId) skillId) (concat 'level' level))}}checked{{/if}}
disabled class="skill-level-checkbox">
</label>
{{/each}} {{/each}}
</div> </div>
</div> </div>
<span class="skill-value">{{lookup (lookup @root.system.stats statId) skillId 'value'}}</span> <span class="skill-value">{{lookup @root.system.stats statId skillId 'value'}}</span>
</div> </div>
{{/if}} {{/if}}
{{/each}} {{/each}}

View File

@@ -18,6 +18,9 @@
<div class="item-controls"> <div class="item-controls">
{{#unless ../isEditMode}} {{#unless ../isEditMode}}
<a data-action="attack" data-item-id="{{item.id}}" title="{{localize 'CELESTOPOL.Combat.attack'}}"><i class="fas fa-khanda"></i></a> <a data-action="attack" data-item-id="{{item.id}}" title="{{localize 'CELESTOPOL.Combat.attack'}}"><i class="fas fa-khanda"></i></a>
{{#if (eq item.system.type "distance")}}
<a data-action="rangedDefense" data-item-id="{{item.id}}" title="{{localize 'CELESTOPOL.Combat.rangedDefenseTitle'}}"><i class="fas fa-shield-halved"></i></a>
{{/if}}
{{/unless}} {{/unless}}
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a> <a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}} {{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}

View File

@@ -13,32 +13,12 @@
<td class="faction-value"> <td class="faction-value">
<div class="faction-checkboxes-container"> <div class="faction-checkboxes-container">
<div class="faction-checkboxes"> <div class="faction-checkboxes">
{{#each (array 1 2 3 4 5 6 7 8 9) as |level|}} {{#each (range 9) as |level|}}
{{#if @root.isEditMode}} <span class="faction-dot {{#if (lte level (lookup @root.system.factions factionId 'value'))}}filled{{/if}}"
<label class="faction-checkbox-wrapper"> {{#if @root.isEditable}}data-action="factionLevel" data-faction="{{factionId}}" data-index="{{level}}"{{/if}}></span>
<input type="checkbox" name="system.factions.{{factionId}}.level{{level}}"
{{#if (lookup (lookup @root.system.factions factionId) (concat 'level' level))}}checked{{/if}}
class="faction-checkbox"
data-faction="{{factionId}}"
data-level="{{level}}">
</label>
{{else}}
<label class="faction-checkbox-wrapper">
<input type="checkbox"
{{#if (lookup (lookup @root.system.factions factionId) (concat 'level' level))}}checked{{/if}}
disabled class="faction-checkbox">
</label>
{{/if}}
{{/each}} {{/each}}
</div> </div>
<span class="faction-count"> <span class="faction-count">{{lookup @root.system.factions factionId 'value'}}</span>
{{#if ../isEditMode}}
<input type="number" name="system.factions.{{factionId}}.value"
value="{{lookup (lookup ../system.factions factionId) 'value'}}" min="0" max="9" class="faction-value-input">
{{else}}
{{lookup (lookup ../system.factions factionId) 'value'}}
{{/if}}
</span>
</div> </div>
</td> </td>
</tr> </tr>
@@ -58,32 +38,12 @@
<td> <td>
<div class="faction-checkboxes-container"> <div class="faction-checkboxes-container">
<div class="faction-checkboxes"> <div class="faction-checkboxes">
{{#each (array 1 2 3 4 5 6 7 8 9) as |level|}} {{#each (range 9) as |level|}}
{{#if ../isEditMode}} <span class="faction-dot {{#if (lte level ../system.factions.perso1.value)}}filled{{/if}}"
<label class="faction-checkbox-wrapper"> {{#if ../isEditable}}data-action="factionLevel" data-faction="perso1" data-index="{{level}}"{{/if}}></span>
<input type="checkbox" name="system.factions.perso1.level{{level}}"
{{#if (lookup ../system.factions.perso1 (concat 'level' level))}}checked{{/if}}
class="faction-checkbox"
data-faction="perso1"
data-level="{{level}}">
</label>
{{else}}
<label class="faction-checkbox-wrapper">
<input type="checkbox"
{{#if (lookup ../system.factions.perso1 (concat 'level' level))}}checked{{/if}}
disabled class="faction-checkbox">
</label>
{{/if}}
{{/each}} {{/each}}
</div> </div>
<span class="faction-count"> <span class="faction-count">{{system.factions.perso1.value}}</span>
{{#if ../isEditMode}}
<input type="number" name="system.factions.perso1.value"
value="{{system.factions.perso1.value}}" min="0" max="9" class="faction-value-input">
{{else}}
{{system.factions.perso1.value}}
{{/if}}
</span>
</div> </div>
</td> </td>
</tr> </tr>
@@ -100,32 +60,12 @@
<td> <td>
<div class="faction-checkboxes-container"> <div class="faction-checkboxes-container">
<div class="faction-checkboxes"> <div class="faction-checkboxes">
{{#each (array 1 2 3 4 5 6 7 8 9) as |level|}} {{#each (range 9) as |level|}}
{{#if ../isEditMode}} <span class="faction-dot {{#if (lte level ../system.factions.perso2.value)}}filled{{/if}}"
<label class="faction-checkbox-wrapper"> {{#if ../isEditable}}data-action="factionLevel" data-faction="perso2" data-index="{{level}}"{{/if}}></span>
<input type="checkbox" name="system.factions.perso2.level{{level}}"
{{#if (lookup ../system.factions.perso2 (concat 'level' level))}}checked{{/if}}
class="faction-checkbox"
data-faction="perso2"
data-level="{{level}}">
</label>
{{else}}
<label class="faction-checkbox-wrapper">
<input type="checkbox"
{{#if (lookup ../system.factions.perso2 (concat 'level' level))}}checked{{/if}}
disabled class="faction-checkbox">
</label>
{{/if}}
{{/each}} {{/each}}
</div> </div>
<span class="faction-count"> <span class="faction-count">{{system.factions.perso2.value}}</span>
{{#if ../isEditMode}}
<input type="number" name="system.factions.perso2.value"
value="{{system.factions.perso2.value}}" min="0" max="9" class="faction-value-input">
{{else}}
{{system.factions.perso2.value}}
{{/if}}
</span>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -70,6 +70,18 @@
{{/if}} {{/if}}
</div> </div>
{{/each}} {{/each}}
{{#with (lookup woundLevels system.blessures.lvl) as |wound|}}
{{#if wound.id}}
<div class="header-stat wound-status-badge wound-level-{{wound.id}}">
<label>{{localize "CELESTOPOL.Wound.status"}}</label>
<span class="wound-value">
<span class="wound-label">{{localize wound.label}}</span>
{{#if wound.duration}}<span class="wound-duration"> — {{localize wound.duration}}</span>{{/if}}
{{#if wound.malus}}<span class="wound-malus"> ({{wound.malus}})</span>{{/if}}
</span>
</div>
{{/if}}
{{/with}}
</div> </div>
</div> </div>

View File

@@ -123,7 +123,11 @@
<span class="result-icon">✦✦</span> <span class="result-icon">✦✦</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.criticalSuccess"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.criticalSuccess"}}</span>
{{#if isCombat}} {{#if isCombat}}
{{#if isRangedDefense}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}</span>
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span> <span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span>
{{/if}}
{{else}} {{else}}
<span class="result-desc">{{localize "CELESTOPOL.Roll.criticalSuccessDesc"}}</span> <span class="result-desc">{{localize "CELESTOPOL.Roll.criticalSuccessDesc"}}</span>
{{/if}} {{/if}}
@@ -131,13 +135,23 @@
<span class="result-icon">✦</span> <span class="result-icon">✦</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.success"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.success"}}</span>
{{#if isCombat}} {{#if isCombat}}
{{#if isRangedDefense}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}</span>
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span> <span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span>
{{/if}} {{/if}}
{{/if}}
{{else if isCriticalFailure}} {{else if isCriticalFailure}}
<span class="result-icon">✖✖</span> <span class="result-icon">✖✖</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.criticalFailure"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.criticalFailure"}}</span>
{{#if isCombat}} {{#if isCombat}}
<span class="result-desc">{{#if (eq weaponType "melee")}}{{localize "CELESTOPOL.Combat.failureHit"}}{{else}}{{localize "CELESTOPOL.Combat.distanceNoWound"}}{{/if}}</span> {{#if (eq weaponType "melee")}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.failureHit"}}</span>
{{else if isRangedDefense}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}</span>
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.distanceNoWound"}}</span>
{{/if}}
{{else}} {{else}}
<span class="result-desc">{{localize "CELESTOPOL.Roll.criticalFailureDesc"}}</span> <span class="result-desc">{{localize "CELESTOPOL.Roll.criticalFailureDesc"}}</span>
{{/if}} {{/if}}
@@ -145,7 +159,13 @@
<span class="result-icon">✖</span> <span class="result-icon">✖</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span>
{{#if isCombat}} {{#if isCombat}}
<span class="result-desc">{{#if (eq weaponType "melee")}}{{localize "CELESTOPOL.Combat.failureHit"}}{{else}}{{localize "CELESTOPOL.Combat.distanceNoWound"}}{{/if}}</span> {{#if (eq weaponType "melee")}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.failureHit"}}</span>
{{else if isRangedDefense}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}</span>
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.distanceNoWound"}}</span>
{{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
</div> </div>
@@ -154,7 +174,7 @@
{{#if woundTaken}} {{#if woundTaken}}
<div class="resistance-wound-notice"> <div class="resistance-wound-notice">
<span class="wound-icon">🩹</span> <span class="wound-icon">🩹</span>
<span>{{#if isCombat}}{{localize "CELESTOPOL.Combat.playerWounded"}}{{else}}{{localize "CELESTOPOL.Roll.woundTaken"}}{{/if}}</span> <span>{{#if isCombat}}{{#if isRangedDefense}}{{localize "CELESTOPOL.Combat.rangedDefensePlayerWounded"}}{{else}}{{localize "CELESTOPOL.Combat.playerWounded"}}{{/if}}{{else}}{{localize "CELESTOPOL.Roll.woundTaken"}}{{/if}}</span>
</div> </div>
{{/if}} {{/if}}

View File

@@ -4,22 +4,16 @@
<span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span> <span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span>
</div> </div>
<div class="track-boxes"> <div class="track-boxes">
{{#each (array "b1" "b2" "b3" "b4" "b5" "b6" "b7" "b8") as |key|}} {{#each (range 8) as |lvl|}}
<div class="track-box {{#if (lookup ../system.blessures key 'checked')}}checked{{/if}}"> <div class="track-box {{#if (lte lvl ../system.blessures.lvl)}}filled{{/if}}"
<input type="checkbox" name="system.blessures.{{key}}.checked" {{#if ../isEditable}}data-action="trackBox" data-path="system.blessures.lvl" data-index="{{lvl}}"{{/if}}>
{{#if (lookup ../system.blessures key 'checked')}}checked{{/if}} <span class="box-label">{{lookup @root.woundLevels lvl 'malus'}}</span>
{{#unless ../isEditable}}disabled{{/unless}}>
<label class="box-label">{{lookup ../system.blessures key 'malus'}}</label>
</div> </div>
{{/each}} {{/each}}
</div> </div>
<div class="track-level"> <div class="track-level">
<label>{{localize "CELESTOPOL.Track.level"}}</label> <label>{{localize "CELESTOPOL.Track.level"}}</label>
{{#if isEditMode}} <span>{{system.blessures.lvl}}</span>
<input type="number" name="system.blessures.lvl" value="{{system.blessures.lvl}}" min="0" max="8">
{{else}}
<span>{{system.blessures.lvl}}</span>
{{/if}}
</div> </div>
</section> </section>

View File

@@ -37,6 +37,18 @@
<span class="anomaly-type-display">{{localize (lookup (lookup anomalyTypes system.anomaly.type) 'label')}}</span> <span class="anomaly-type-display">{{localize (lookup (lookup anomalyTypes system.anomaly.type) 'label')}}</span>
{{/if}} {{/if}}
</div> </div>
{{#with (lookup woundLevels system.blessures.lvl) as |wound|}}
{{#if wound.id}}
<div class="header-stat wound-status-badge wound-level-{{wound.id}}">
<label>{{localize "CELESTOPOL.Wound.status"}}</label>
<span class="wound-value">
<span class="wound-label">{{localize wound.label}}</span>
{{#if wound.duration}}<span class="wound-duration"> — {{localize wound.duration}}</span>{{/if}}
{{#if wound.malus}}<span class="wound-malus"> ({{wound.malus}})</span>{{/if}}
</span>
</div>
{{/if}}
{{/with}}
</div> </div>
</div> </div>
<div class="header-buttons"> <div class="header-buttons">

View File

@@ -6,9 +6,13 @@
{{!-- Arme (mode combat) --}} {{!-- Arme (mode combat) --}}
{{#if isCombat}} {{#if isCombat}}
<div class="roll-weapon-line"> <div class="roll-weapon-line">
<span class="weapon-icon"></span> <span class="weapon-icon">{{#if isRangedDefense}}🛡{{else}}{{/if}}</span>
<span class="weapon-name">{{weaponName}}</span> <span class="weapon-name">{{weaponName}}</span>
{{#if isRangedDefense}}
<span class="weapon-tag ranged-defense">{{localize "CELESTOPOL.Combat.rangedDefenseTag"}}</span>
{{else}}
<span class="weapon-degats">+{{weaponDegats}}</span> <span class="weapon-degats">+{{weaponDegats}}</span>
{{/if}}
</div> </div>
{{/if}} {{/if}}
<div class="roll-skill-line"> <div class="roll-skill-line">