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 = ` +
+
+ + +
+
+ + +
+

${i18n.format("CELESTOPOL.XP.disponible", { n: currentXp })}

+
` + + 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 --}} +
+
{{localize "CELESTOPOL.XP.title"}}
+ +
+
+ + {{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}} + + + + + + + {{#if isEditMode}}{{/if}} + + + + {{#each system.xp.log}} + + + + + {{#if ../isEditMode}} + + {{/if}} + + {{/each}} + +
{{localize "CELESTOPOL.XP.date"}}{{localize "CELESTOPOL.XP.raison"}}{{localize "CELESTOPOL.XP.montant"}}
{{this.date}}{{this.raison}}−{{this.montant}} + +
+ {{/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 --}}
{{localize "CELESTOPOL.Actor.description"}}
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 @@
{{localize "CELESTOPOL.Track.blessures"}} {{localize "CELESTOPOL.Track.currentMalus"}} : - {{system.blessures.lvl}} + {{lookup @root.woundLevels system.blessures.lvl 'malus'}}
- {{#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 --}}
- {{localize "CELESTOPOL.Track.destin"}} + {{localize "CELESTOPOL.Track.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}}
- {{#each (array 1 2 3 4 5 6 7 8) as |level|}} - + {{#each (range 8) as |lvl|}} + {{/each}}
- + {{lookup @root.system.stats statId skillId 'value'}}
{{else}}
{{localize skill.label}}
- {{#each (array 1 2 3 4 5 6 7 8) as |level|}} - + {{#each (range 8) as |lvl|}} + {{/each}}
- {{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 @@
- {{#each (array 1 2 3 4 5 6 7 8 9) as |level|}} - {{#if @root.isEditMode}} - - {{else}} - - {{/if}} + {{#each (range 9) as |level|}} + {{/each}}
- - {{#if ../isEditMode}} - - {{else}} - {{lookup (lookup ../system.factions factionId) 'value'}} - {{/if}} - + {{lookup @root.system.factions factionId 'value'}}
@@ -58,32 +38,12 @@
- {{#each (array 1 2 3 4 5 6 7 8 9) as |level|}} - {{#if ../isEditMode}} - - {{else}} - - {{/if}} + {{#each (range 9) as |level|}} + {{/each}}
- - {{#if ../isEditMode}} - - {{else}} - {{system.factions.perso1.value}} - {{/if}} - + {{system.factions.perso1.value}}
@@ -100,32 +60,12 @@
- {{#each (array 1 2 3 4 5 6 7 8 9) as |level|}} - {{#if ../isEditMode}} - - {{else}} - - {{/if}} + {{#each (range 9) as |level|}} + {{/each}}
- - {{#if ../isEditMode}} - - {{else}} - {{system.factions.perso2.value}} - {{/if}} - + {{system.factions.perso2.value}}
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}} +
+ + + {{localize wound.label}} + {{#if wound.duration}} — {{localize wound.duration}}{{/if}} + {{#if wound.malus}} ({{wound.malus}}){{/if}} + +
+ {{/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}} +
+ + + {{localize wound.label}} + {{#if wound.duration}} — {{localize wound.duration}}{{/if}} + {{#if wound.malus}} ({{wound.malus}}){{/if}} + +
+ {{/if}} + {{/with}}
diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs index 80598e4..0b54fe7 100644 --- a/templates/roll-dialog.hbs +++ b/templates/roll-dialog.hbs @@ -6,9 +6,13 @@ {{!-- Arme (mode combat) --}} {{#if isCombat}}
- + {{#if isRangedDefense}}🛡{{else}}⚔{{/if}} {{weaponName}} + {{#if isRangedDefense}} + {{localize "CELESTOPOL.Combat.rangedDefenseTag"}} + {{else}} +{{weaponDegats}} + {{/if}}
{{/if}}