From f9ddcdf9dad222daf9d2ada22e7d2a780955acb0 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Sat, 28 Mar 2026 18:15:06 +0100 Subject: [PATCH] =?UTF-8?q?Refonte=20compl=C3=A8te=20du=20syst=C3=A8me=20A?= =?UTF-8?q?nomalies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DataModel : renommage value→level (1-4), ajout usesRemaining (0-4), suppression scores/notes - Config : ajout ANOMALY_DEFINITIONS avec compétences applicables par type (8 anomalies) - Fiche item anomalie : header avec level/uses visuels (dots), barre de compétences applicables, 2 onglets Description + Technique/Narratif (suppression onglet Scores) - Fiche PJ onglet Domaines : bloc anomalie proéminent unique avec: - Nom + sous-type + icône - Dots niveau (●●○○) - Dots usages + bouton Utiliser + bouton Réinitialiser - Chips des domaines applicables - Actions : useAnomaly (décrémente usesRemaining), resetAnomalyUses (reset au niveau) - Contrainte : max 1 anomalie par personnage (drop + createAnomaly) - Helpers HBS : lte, gte, lt ajoutés - i18n : nouvelles clés Anomaly.* (level, usesRemaining, use, resetUses, etc.) - CSS : .anomaly-block sur fiche PJ, dots animés, .anomaly-uses-row sur fiche item Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fvtt-celestopol.mjs | 11 +- lang/fr.json | 10 + .../applications/sheets/base-actor-sheet.mjs | 4 + .../applications/sheets/character-sheet.mjs | 47 ++++- module/applications/sheets/item-sheets.mjs | 13 +- module/config/system.mjs | 18 ++ module/models/items.mjs | 17 +- styles/character.less | 183 ++++++++++++++++++ styles/items.less | 84 ++++++++ templates/anomaly.hbs | 46 ++++- templates/character-competences.hbs | 82 ++++++-- 11 files changed, 474 insertions(+), 41 deletions(-) diff --git a/fvtt-celestopol.mjs b/fvtt-celestopol.mjs index ddfa16d..dc1608f 100644 --- a/fvtt-celestopol.mjs +++ b/fvtt-celestopol.mjs @@ -55,7 +55,7 @@ Hooks.once("init", () => { CONFIG.Actor.trackableAttributes = { character: { bar: ["blessures.lvl"], - value: ["initiative", "anomaly.value"], + value: ["initiative", "anomaly.level"], }, npc: { bar: ["blessures.lvl"], @@ -132,6 +132,15 @@ function _registerHandlebarsHelpers() { // Helper : greater than Handlebars.registerHelper("gt", (a, b) => a > b) + // Helper : less than or equal + Handlebars.registerHelper("lte", (a, b) => a <= b) + + // Helper : greater than or equal + Handlebars.registerHelper("gte", (a, b) => a >= b) + + // Helper : less than + Handlebars.registerHelper("lt", (a, b) => a < b) + // Helper : logical OR Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean)) diff --git a/lang/fr.json b/lang/fr.json index 07865df..d732b55 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -47,6 +47,15 @@ }, "Anomaly": { "type": "Type d'anomalie", + "level": "Niveau", + "usesRemaining": "Utilisations restantes", + "use": "Utiliser l'anomalie", + "resetUses": "Réinitialiser les utilisations (nouveau scénario)", + "noAnomaly": "Aucune anomalie", + "noUsesLeft": "Plus d'utilisations disponibles pour ce scénario", + "maxAnomaly": "Un personnage ne peut avoir qu'une seule anomalie", + "applicableSkills": "Domaines applicables", + "moonDie": "Dé de lune", "none": "Aucune", "entropie": "Entropie", "communicationaveclesmorts": "Communication avec les morts", @@ -143,6 +152,7 @@ "extreme": "Extrême" }, "Item": { + "anomaly": "Anomalie", "anomalies": "Anomalies", "aspects": "Aspects", "attributes": "Attributs", diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs index aa892de..57f807c 100644 --- a/module/applications/sheets/base-actor-sheet.mjs +++ b/module/applications/sheets/base-actor-sheet.mjs @@ -108,6 +108,10 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou } async _onDropItem(item) { + if (item.type === "anomaly" && this.document.itemTypes.anomaly.length > 0) { + ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly")) + return + } await this.document.createEmbeddedDocuments("Item", [item.toObject()], { renderSheet: false }) } diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index a605a9f..9d90a4e 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -8,10 +8,12 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { position: { width: 920, height: 660 }, window: { contentClasses: ["character-content"] }, actions: { - createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly, - createAspect: CelestopolCharacterSheet.#onCreateAspect, - createAttribute: CelestopolCharacterSheet.#onCreateAttribute, - createEquipment: CelestopolCharacterSheet.#onCreateEquipment, + createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly, + createAspect: CelestopolCharacterSheet.#onCreateAspect, + createAttribute: CelestopolCharacterSheet.#onCreateAttribute, + createEquipment: CelestopolCharacterSheet.#onCreateEquipment, + useAnomaly: CelestopolCharacterSheet.#onUseAnomaly, + resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses, }, } @@ -64,9 +66,21 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { case "competences": context.tab = context.tabs.competences - context.anomalies = doc.itemTypes.anomaly + context.anomaly = doc.itemTypes.anomaly[0] ?? null context.aspects = doc.itemTypes.aspect context.attributes = doc.itemTypes.attribute + if (context.anomaly) { + const def = SYSTEM.ANOMALY_DEFINITIONS[context.anomaly.system.subtype] ?? SYSTEM.ANOMALY_DEFINITIONS.none + context.anomalySkillLabels = def.technicalSkills.map(key => { + if (key === "lune") return game.i18n.localize("CELESTOPOL.Anomaly.moonDie") + for (const skills of Object.values(SYSTEM.SKILLS)) { + if (skills[key]) return game.i18n.localize(skills[key].label) + } + return key + }) + } else { + context.anomalySkillLabels = [] + } break case "blessures": @@ -91,6 +105,10 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { } static #onCreateAnomaly() { + if (this.document.itemTypes.anomaly.length > 0) { + ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly")) + return + } this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly", }]) @@ -113,4 +131,23 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { name: game.i18n.localize("CELESTOPOL.Item.newEquipment"), type: "equipment", }]) } + + static async #onUseAnomaly(event, target) { + const itemId = target.dataset.itemId + const anomaly = this.document.items.get(itemId) + if (!anomaly) return + const current = anomaly.system.usesRemaining + if (current <= 0) { + ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.noUsesLeft")) + return + } + await anomaly.update({ "system.usesRemaining": current - 1 }) + } + + static async #onResetAnomalyUses(event, target) { + const itemId = target.dataset.itemId + const anomaly = this.document.items.get(itemId) + if (!anomaly) return + await anomaly.update({ "system.usesRemaining": anomaly.system.level }) + } } diff --git a/module/applications/sheets/item-sheets.mjs b/module/applications/sheets/item-sheets.mjs index cb86ef1..2664346 100644 --- a/module/applications/sheets/item-sheets.mjs +++ b/module/applications/sheets/item-sheets.mjs @@ -4,7 +4,7 @@ import { SYSTEM } from "../../config/system.mjs" export class CelestopolAnomalySheet extends CelestopolItemSheet { static DEFAULT_OPTIONS = { classes: ["anomaly"], - position: { width: 620, height: 560 }, + position: { width: 560, height: 460 }, } static PARTS = { main: { template: "systems/fvtt-celestopol/templates/anomaly.hbs" }, @@ -12,7 +12,16 @@ export class CelestopolAnomalySheet extends CelestopolItemSheet { async _prepareContext() { const ctx = await super._prepareContext() ctx.anomalyTypes = SYSTEM.ANOMALY_TYPES - ctx.skills = SYSTEM.SKILLS + + const def = SYSTEM.ANOMALY_DEFINITIONS[ctx.system.subtype] ?? SYSTEM.ANOMALY_DEFINITIONS.none + ctx.applicableSkillLabels = def.technicalSkills.map(key => { + if (key === "lune") return game.i18n.localize("CELESTOPOL.Anomaly.moonDie") + for (const skills of Object.values(SYSTEM.SKILLS)) { + if (skills[key]) return game.i18n.localize(skills[key].label) + } + return key + }) + ctx.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( this.document.system.description, { async: true }) ctx.enrichedTechnique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( diff --git a/module/config/system.mjs b/module/config/system.mjs index 2c5402c..1479645 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -59,6 +59,23 @@ export const ANOMALY_TYPES = { voyageastral: { id: "voyageastral", label: "CELESTOPOL.Anomaly.voyageastral" }, } +/** + * Définitions des anomalies : compétences applicables pour l'usage Technique. + * "lune" est une clé spéciale désignant le dé de lune (Entropie). + * Les autres clés correspondent aux identifiants de domaine dans SKILLS. + */ +export const ANOMALY_DEFINITIONS = { + none: { technicalSkills: [] }, + entropie: { technicalSkills: ["lune"] }, + communicationaveclesmorts:{ technicalSkills: ["instruction", "mtechnologique", "raisonnement", "traitement"] }, + telekinesie: { technicalSkills: ["echauffouree", "effacement", "mobilite", "prouesse"] }, + telepathie: { technicalSkills: ["appreciation", "attraction", "echauffouree", "faveur"] }, + tarotdivinatoire: { technicalSkills: ["appreciation", "arts", "inspiration", "traque"] }, + illusion: { technicalSkills: ["coercition", "echauffouree", "effacement", "traque"] }, + suggestion: { technicalSkills: ["artifice", "attraction", "coercition", "faveur"] }, + voyageastral: { technicalSkills: ["appreciation", "mtechnologique", "traitement", "traque"] }, +} + /** Factions du monde de Célestopol. */ export const FACTIONS = { pinkerton: { id: "pinkerton", label: "CELESTOPOL.Faction.pinkerton" }, @@ -122,6 +139,7 @@ export const SYSTEM = { SKILLS, ALL_SKILLS, ANOMALY_TYPES, + ANOMALY_DEFINITIONS, FACTIONS, WOUND_LEVELS, DIFFICULTY_CHOICES, diff --git a/module/models/items.mjs b/module/models/items.mjs index 2be4c07..1fde29e 100644 --- a/module/models/items.mjs +++ b/module/models/items.mjs @@ -31,15 +31,14 @@ export class CelestopolAnomaly extends foundry.abstract.TypeDataModel { const fields = foundry.data.fields const reqInt = { required: true, nullable: false, integer: true } return { - subtype: new fields.StringField({ required: true, nullable: false, initial: "none", - choices: Object.keys(SYSTEM.ANOMALY_TYPES) }), - value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), - reference: new fields.StringField({ required: true, nullable: false, initial: "" }), - scores: skillScoresSchema(), - description: new fields.HTMLField({ required: true, textSearch: true }), - technique: new fields.HTMLField({ required: true, textSearch: true }), - narratif: new fields.HTMLField({ required: true, textSearch: true }), - notes: new fields.HTMLField({ required: true, textSearch: true }), + subtype: new fields.StringField({ required: true, nullable: false, initial: "none", + choices: Object.keys(SYSTEM.ANOMALY_TYPES) }), + level: new fields.NumberField({ ...reqInt, initial: 2, min: 1, max: 4 }), + usesRemaining: new fields.NumberField({ ...reqInt, initial: 2, min: 0, max: 4 }), + reference: new fields.StringField({ required: true, nullable: false, initial: "" }), + description: new fields.HTMLField({ required: true, textSearch: true }), + technique: new fields.HTMLField({ required: true, textSearch: true }), + narratif: new fields.HTMLField({ required: true, textSearch: true }), } } } diff --git a/styles/character.less b/styles/character.less index 0a7213c..fe2181e 100644 --- a/styles/character.less +++ b/styles/character.less @@ -250,4 +250,187 @@ .section-header { .cel-section-header(); } .enriched-html { font-size: 0.9em; line-height: 1.6; } } + + // ── Bloc Anomalie sur l'onglet Domaines ────────────────────────────────── + .anomaly-block { + border: 1px solid rgba(196,154,26,0.5); + border-radius: 4px; + margin-bottom: 12px; + overflow: hidden; + + .anomaly-block-header { + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px 10px; + border-bottom: 2px solid var(--cel-orange); + + .anomaly-block-title { + font-family: var(--cel-font-title); + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--cel-orange); + } + a { color: var(--cel-orange-light); &:hover { color: var(--cel-orange); } } + } + + .anomaly-empty { + padding: 12px; + text-align: center; + color: var(--cel-border); + font-style: italic; + font-size: 0.85em; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + i { font-size: 1.1em; } + } + + .anomaly-content { + padding: 8px 10px; + background: rgba(240,232,212,0.06); + + .anomaly-info-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 7px; + + .anomaly-icon { + width: 40px; + height: 40px; + object-fit: cover; + border: 1px solid var(--cel-orange); + border-radius: 3px; + } + + .anomaly-details { + flex: 1; + .anomaly-name { + font-family: var(--cel-font-title); + font-size: 1em; + color: var(--cel-orange); + font-weight: bold; + } + .anomaly-subtype { + font-size: 0.75em; + color: var(--cel-orange-light); + text-transform: uppercase; + letter-spacing: 0.05em; + } + } + + .anomaly-controls { + display: flex; + gap: 6px; + a { color: var(--cel-border); font-size: 0.9em; &:hover { color: var(--cel-orange); } } + } + } + + .anomaly-level-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; + + .anomaly-level-label { + font-size: 0.72em; + text-transform: uppercase; + color: var(--cel-orange-light); + white-space: nowrap; + } + + .anomaly-level-dots { + display: flex; + gap: 4px; + .anomaly-level-dot { + width: 10px; + height: 10px; + border-radius: 50%; + border: 2px solid var(--cel-orange); + &.active { background: var(--cel-orange); } + &.inactive { background: transparent; border-color: rgba(196,154,26,0.25); } + } + } + } + + .anomaly-uses-row { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + + .anomaly-uses-label { + font-size: 0.72em; + text-transform: uppercase; + color: var(--cel-orange-light); + white-space: nowrap; + } + + .anomaly-uses-dots { + display: flex; + gap: 4px; + .anomaly-dot { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid var(--cel-orange); + &.available { background: var(--cel-orange); } + &.spent { background: transparent; } + &.inactive { background: transparent; border-color: rgba(196,154,26,0.25); } + } + } + + .anomaly-use-btn { + background: var(--cel-green); + border: 1px solid var(--cel-orange); + color: var(--cel-orange); + font-size: 0.72em; + padding: 2px 8px; + cursor: pointer; + font-family: var(--cel-font-title); + text-transform: uppercase; + letter-spacing: 0.04em; + border-radius: 2px; + transition: background 0.15s; + &:hover:not(:disabled) { background: var(--cel-green-light); } + &:disabled { opacity: 0.4; cursor: default; } + } + + .anomaly-reset-btn { + color: var(--cel-border); + font-size: 0.9em; + &:hover { color: var(--cel-orange); } + } + } + + .anomaly-skills { + margin-top: 6px; + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; + + .anomaly-skills-label { + font-size: 0.7em; + color: var(--cel-border); + text-transform: uppercase; + } + + .anomaly-skill-chip { + background: rgba(196,154,26,0.1); + border: 1px solid rgba(196,154,26,0.3); + border-radius: 3px; + padding: 1px 5px; + font-size: 0.7em; + color: var(--cel-orange-light); + } + } + } + } } diff --git a/styles/items.less b/styles/items.less index 8456071..dc39f89 100644 --- a/styles/items.less +++ b/styles/items.less @@ -165,6 +165,90 @@ } } + // ── Anomaly-specific styles ─────────────────────────────────────────────── + + &.anomaly { + .anomaly-level-field { + display: flex; + align-items: center; + gap: 4px; + label { color: var(--cel-orange-light); font-size: 0.75em; text-transform: uppercase; } + .level-input, .anomaly-level-value { + width: 38px; + background: transparent; + border: 1px solid var(--cel-orange-light); + color: var(--cel-orange); + text-align: center; + font-size: 1.1em; + font-weight: bold; + } + } + + .anomaly-uses-row { + display: flex; + align-items: center; + gap: 6px; + margin-top: 5px; + padding-top: 4px; + border-top: 1px solid rgba(196,154,26,0.2); + + .anomaly-uses-label { + color: var(--cel-orange-light); + font-size: 0.7em; + text-transform: uppercase; + white-space: nowrap; + } + + .anomaly-uses-dots { display: flex; gap: 4px; } + + .uses-number-input { + width: 34px; + background: transparent; + border: 1px solid rgba(196,154,26,0.4); + color: var(--cel-orange); + text-align: center; + font-size: 0.85em; + } + } + + .anomaly-dot { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid var(--cel-orange); + display: inline-block; + &.available { background: var(--cel-orange); } + &.spent { background: transparent; } + &.inactive { background: transparent; border-color: rgba(196,154,26,0.25); } + } + + .anomaly-skills-bar { + background: rgba(0,0,0,0.18); + border-bottom: 1px solid rgba(196,154,26,0.2); + padding: 5px 10px; + display: flex; + flex-wrap: wrap; + gap: 5px; + align-items: center; + + .anomaly-skills-label { + color: var(--cel-orange-light); + font-size: 0.72em; + text-transform: uppercase; + margin-right: 2px; + } + + .anomaly-skill-chip { + background: rgba(196,154,26,0.12); + border: 1px solid rgba(196,154,26,0.35); + border-radius: 3px; + padding: 1px 6px; + font-size: 0.72em; + color: var(--cel-orange-light); + } + } + } + // Equipment-specific &.equipment { .equipment-stats { diff --git a/templates/anomaly.hbs b/templates/anomaly.hbs index 1ccbae8..6cc27a6 100644 --- a/templates/anomaly.hbs +++ b/templates/anomaly.hbs @@ -11,19 +11,51 @@ {{/each}} -
- - +
+ + {{#if isEditable}} + + {{else}} + {{system.level}} + {{/if}}
+ {{!-- Usages restants : dots visuels --}} +
+ {{localize "CELESTOPOL.Anomaly.usesRemaining"}} : +
+ {{#each (array 1 2 3 4) as |n|}} + {{#if (lte n ../system.usesRemaining)}} + + {{else}} + {{#if (lte n ../system.level)}} + + {{else}} + + {{/if}} + {{/if}} + {{/each}} +
+ {{#if isEditable}} + + {{/if}} +
+ {{!-- Domaines applicables --}} + {{#if applicableSkillLabels.length}} +
+ {{localize "CELESTOPOL.Anomaly.applicableSkills"}} : + {{#each applicableSkillLabels as |label|}} + {{label}} + {{/each}} +
+ {{/if}} +
@@ -47,8 +79,4 @@ {{editor system.narratif target="system.narratif" button=true editable=isEditable}}
- -
- {{> "systems/fvtt-celestopol/templates/partials/item-scores.hbs" skills=skills stats=stats system=system}} -
diff --git a/templates/character-competences.hbs b/templates/character-competences.hbs index 7d5ec30..f9d5a24 100644 --- a/templates/character-competences.hbs +++ b/templates/character-competences.hbs @@ -58,27 +58,79 @@ {{/each}} - {{!-- Items : Anomalies, Aspects, Attributs --}} + {{!-- Items : Anomalie (unique), Aspects, Attributs --}}
- {{!-- Anomalies --}} -
-
- {{localize "CELESTOPOL.Item.anomalies"}} + {{!-- Anomalie : bloc proéminent unique --}} +
+
+ {{localize "CELESTOPOL.Item.anomaly"}} {{#if isEditMode}} - + {{#unless anomaly}} + + {{/unless}} {{/if}}
- {{#each anomalies as |item|}} -
- {{item.name}} - {{item.name}} - {{item.system.value}} -
- - {{#if ../isEditMode}}{{/if}} + {{#if anomaly}} +
+
+ {{anomaly.name}} +
+
{{anomaly.name}}
+
{{localize (lookup (lookup anomalyTypes anomaly.system.subtype) 'label')}}
+
+
+ + {{#if isEditMode}}{{/if}} +
+
+ {{localize "CELESTOPOL.Anomaly.level"}} : +
+ {{#each (array 1 2 3 4) as |n|}} + + {{/each}} +
+
+
+ {{localize "CELESTOPOL.Anomaly.usesRemaining"}} : +
+ {{#each (array 1 2 3 4) as |n|}} + {{#if (lte n ../anomaly.system.usesRemaining)}} + + {{else}} + {{#if (lte n ../anomaly.system.level)}} + + {{else}} + + {{/if}} + {{/if}} + {{/each}} +
+ + + + +
+ {{#if anomalySkillLabels.length}} +
+ {{localize "CELESTOPOL.Anomaly.applicableSkills"}} : + {{#each anomalySkillLabels as |label|}} + {{label}} + {{/each}} +
+ {{/if}}
- {{/each}} + {{else}} +
+ + {{localize "CELESTOPOL.Anomaly.noAnomaly"}} +
+ {{/if}}
{{!-- Aspects --}}