diff --git a/_source/anomalies/communicationaveclesmorts.json b/_source/anomalies/communicationaveclesmorts.json index 6f7bd64..0d18cc6 100644 --- a/_source/anomalies/communicationaveclesmorts.json +++ b/_source/anomalies/communicationaveclesmorts.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test d'une Spécialisation de l'Esprit (Instruction, Merveilleux technologique, Raisonnement, Traitement), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

", - "narratif": "

Le personnage entre en contact avec l'esprit d'un défunt. Il peut lui poser une question fermée (réponse par oui ou par non uniquement). Le contact est bref et les réponses peuvent être fragmentées ou métaphoriques, à la discrétion du narrateur.

", + "narratif": "

Le protagoniste entre en contact avec l'esprit d'un défunt. Il peut lui poser une question fermée (réponse par oui ou par non uniquement). Le contact est bref et les réponses peuvent être fragmentées ou métaphoriques, à la discrétion du narrateur.

", "exemples": "" }, "_key": "!items!anomCommMorts001" diff --git a/_source/anomalies/entropie.json b/_source/anomalies/entropie.json index 8fc1957..042b7a3 100644 --- a/_source/anomalies/entropie.json +++ b/_source/anomalies/entropie.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, le protagoniste gagne la possibilité de relancer le Dé de Lune un nombre de fois égal à son Niveau d'Anomalie. Il peut choisir de conserver le résultat préféré.

Cette capacité ne s'applique pas aux tests de chance.

", - "narratif": "

Le personnage peut influencer le hasard à sa façon, en déclenchant ou en évitant de petits événements aléatoires dans son environnement proche. Ces manifestations sont mineures, subtiles, et ne semblent jamais surnaturelles aux yeux des témoins.

", + "narratif": "

Le protagoniste peut influencer le hasard à sa façon, en déclenchant ou en évitant de petits événements aléatoires dans son environnement proche. Ces manifestations sont mineures, subtiles, et ne semblent jamais surnaturelles aux yeux des témoins.

", "exemples": "" }, "_key": "!items!anomEntropie0001" diff --git a/_source/anomalies/illusion.json b/_source/anomalies/illusion.json index 056dc8d..3b830a7 100644 --- a/_source/anomalies/illusion.json +++ b/_source/anomalies/illusion.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test de Coercition, Échauffourée, Effacement ou Traque, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

", - "narratif": "

Le personnage peut générer une petite illusion mineure — visuelle, auditive ou olfactive, au choix — sans détail ni précision, pour une durée d'une minute. L'illusion ne peut représenter un être vivant en détail et ne résiste pas à un examen rapproché.

", + "narratif": "

Le protagoniste peut générer une petite illusion mineure — visuelle, auditive ou olfactive, au choix — sans détail ni précision, pour une durée d'une minute. L'illusion ne peut représenter un être vivant en détail et ne résiste pas à un examen rapproché.

", "exemples": "" }, "_key": "!items!anomIllusion0001" diff --git a/_source/anomalies/suggestion.json b/_source/anomalies/suggestion.json index 079a38c..702e9cb 100644 --- a/_source/anomalies/suggestion.json +++ b/_source/anomalies/suggestion.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test d'une Spécialisation de l'Âme (Artifice, Attraction, Coercition, Faveur), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

", - "narratif": "

Le personnage est capable d'influencer la prise de décision d'une personne en lui parlant à voix haute et en la regardant dans les yeux. Cette décision doit avoir un impact immédiat sur l'action de la personne concernée.

Cette capacité fonctionne également sur les automates sophistiqués de 4e et 5e génération.

", + "narratif": "

Le protagoniste est capable d'influencer la prise de décision d'une personne en lui parlant à voix haute et en la regardant dans les yeux. Cette décision doit avoir un impact immédiat sur l'action de la personne concernée.

Cette capacité fonctionne également sur les automates sophistiqués de 4e et 5e génération.

", "exemples": "" }, "_key": "!items!anomSuggestion01" diff --git a/_source/anomalies/tarotdivinatoire.json b/_source/anomalies/tarotdivinatoire.json index 2fd7f92..02f141e 100644 --- a/_source/anomalies/tarotdivinatoire.json +++ b/_source/anomalies/tarotdivinatoire.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test d'une Spécialisation du Cœur (Appréciation, Arts, Inspiration, Traque), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

", - "narratif": "

En tirant les cartes, le personnage peut apprendre une information sur une personne concernant son passé, son présent ou son avenir. L'information reste sujette à interprétation et le narrateur peut choisir de la formuler de façon symbolique ou métaphorique.

", + "narratif": "

En tirant les cartes, le protagoniste peut apprendre une information sur une personne concernant son passé, son présent ou son avenir. L'information reste sujette à interprétation et le narrateur peut choisir de la formuler de façon symbolique ou métaphorique.

", "exemples": "" }, "_key": "!items!anomTarot00001" diff --git a/_source/anomalies/telekinesie.json b/_source/anomalies/telekinesie.json index dff85d7..0fbce7f 100644 --- a/_source/anomalies/telekinesie.json +++ b/_source/anomalies/telekinesie.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test d'une Spécialisation du Corps (Échauffourée, Effacement, Mobilité, Prouesse), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

", - "narratif": "

Dans un rayon de 8 mètres, le personnage peut déplacer par la pensée un petit objet léger sans attaches, sur 4 mètres (dans n'importe quelle direction) pendant 2 tours. L'objet doit être visible et accessible par le regard.

", + "narratif": "

Dans un rayon de 8 mètres, le protagoniste peut déplacer par la pensée un petit objet léger sans attaches, sur 4 mètres (dans n'importe quelle direction) pendant 2 tours. L'objet doit être visible et accessible par le regard.

", "exemples": "" }, "_key": "!items!anomTelekines01" diff --git a/_source/anomalies/telepathie.json b/_source/anomalies/telepathie.json index 3aa194a..5e917c0 100644 --- a/_source/anomalies/telepathie.json +++ b/_source/anomalies/telepathie.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test d'Appréciation, Attraction, Échauffourée ou Faveur, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

Cette capacité fonctionne également sur les automates sophistiqués de 4e et 5e génération.

", - "narratif": "

Le personnage est capable de percevoir les pensées superficielles d'un tiers. Il peut comprendre l'état émotionnel d'une personne ou capter une image ou un mot dans son esprit (à la discrétion du narrateur), simplement en l'observant.

", + "narratif": "

Le protagoniste est capable de percevoir les pensées superficielles d'un tiers. Il peut comprendre l'état émotionnel d'une personne ou capter une image ou un mot dans son esprit (à la discrétion du narrateur), simplement en l'observant.

", "exemples": "" }, "_key": "!items!anomTelepathi01" diff --git a/_source/anomalies/voyageastral.json b/_source/anomalies/voyageastral.json index 8c0206e..291a1fe 100644 --- a/_source/anomalies/voyageastral.json +++ b/_source/anomalies/voyageastral.json @@ -8,7 +8,7 @@ "level": 2, "usesRemaining": 2, "technique": "

Durant un scénario, lors d'un test d'Appréciation, Merveilleux technologique, Traitement ou Traque, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.

Il doit conserver le dernier résultat.

", - "narratif": "

L'esprit du personnage quitte son enveloppe corporelle et se déplace de 8 mètres par tour pendant 4 tours, dans n'importe quelle direction. L'esprit est invisible et peut traverser tous les obstacles matériels. Les sens du personnage restent les mêmes durant le voyage.

Le corps reste immobile et vulnérable durant le voyage.

", + "narratif": "

L'esprit du protagoniste quitte son enveloppe corporelle et se déplace de 8 mètres par tour pendant 4 tours, dans n'importe quelle direction. L'esprit est invisible et peut traverser tous les obstacles matériels. Les sens du protagoniste restent les mêmes durant le voyage.

Le corps reste immobile et vulnérable durant le voyage.

", "exemples": "" }, "_key": "!items!anomVoyAstral01" diff --git a/fvtt-celestopol.mjs b/fvtt-celestopol.mjs index c4f76a6..ef81a0e 100644 --- a/fvtt-celestopol.mjs +++ b/fvtt-celestopol.mjs @@ -17,6 +17,7 @@ import { CelestopolActor, CelestopolItem, CelestopolChatMessage, + CelestopolCombat, CelestopolRoll, } from "./module/documents/_module.mjs" import { @@ -47,8 +48,11 @@ Hooks.once("init", () => { } } - // Expose SYSTEM constants in game.system namespace - game.celestopol = { SYSTEM } + // Expose SYSTEM constants + utilities globales + game.celestopol = { + SYSTEM, + rollMoonStandalone: (actor = null) => CelestopolRoll.rollMoonStandalone(actor), + } // ── DataModels ────────────────────────────────────────────────────────── CONFIG.Actor.dataModels.character = CelestopolCharacter @@ -64,8 +68,13 @@ Hooks.once("init", () => { CONFIG.Actor.documentClass = CelestopolActor CONFIG.Item.documentClass = CelestopolItem CONFIG.ChatMessage.documentClass = CelestopolChatMessage + CONFIG.Combat.documentClass = CelestopolCombat CONFIG.Dice.rolls.push(CelestopolRoll) + // ── Initiative déterministe (pas de dé) ───────────────────────────────── + // Formule de secours si Combat.rollInitiative est appelé sans passer par notre override + CONFIG.Combat.initiative = { formula: "@initiative", decimals: 0 } + // ── Token display defaults ─────────────────────────────────────────────── CONFIG.Actor.trackableAttributes = { character: { @@ -131,8 +140,6 @@ Hooks.once("init", () => { /* ─── Ready hook ─────────────────────────────────────────────────────────── */ -/* ─── Ready hook ─────────────────────────────────────────────────────────── */ - Hooks.once("ready", () => { console.log(`${SYSTEM_ID} | System ready`) @@ -279,6 +286,14 @@ function _registerHandlebarsHelpers() { // Helper : add two numbers Handlebars.registerHelper("add", (a, b) => a + b) + // Helper : vrai si le dot lvl correspond au seuil de résistance de la spécialisation + Handlebars.registerHelper("isResThreshold", (skillId, lvl) => { + for (const group of Object.values(SYSTEM.SKILLS)) { + if (group[skillId]) return group[skillId].resThreshold === lvl + } + return false + }) + Handlebars.registerHelper("let", function(value, options) { return options.fn({ value }) }) @@ -318,6 +333,8 @@ function _preloadTemplates() { `${base}/npc-main.hbs`, `${base}/npc-competences.hbs`, `${base}/npc-blessures.hbs`, + `${base}/npc-equipement.hbs`, + `${base}/npc-biographie.hbs`, `${base}/anomaly.hbs`, `${base}/aspect.hbs`, `${base}/equipment.hbs`, @@ -325,6 +342,7 @@ function _preloadTemplates() { `${base}/armure.hbs`, `${base}/roll-dialog.hbs`, `${base}/chat-message.hbs`, + `${base}/moon-standalone.hbs`, `${base}/partials/item-scores.hbs`, ]) } diff --git a/lang/fr.json b/lang/fr.json index 38af1f1..dd28f3f 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1,7 +1,7 @@ { "TYPES": { "Actor": { - "character": "Personnage", + "character": "Protagoniste", "npc": "PNJ" }, "Item": { @@ -18,12 +18,14 @@ "concept": "Concept / Profession", "initiative": "Initiative", "anomaly": "Anomalie", - "description": "Biographie", + "descriptionPhysique": "Description physique", + "descriptionPsychologique": "Description psychologique", "notes": "Notes", "metier": "Métier", "origine": "Origine", "age": "Âge", - "faction": "Faction" + "faction": "Faction", + "description": "Description" }, "Stat": { "res": "Résistance", @@ -58,7 +60,7 @@ "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", + "maxAnomaly": "Un protagoniste ne peut avoir qu'une seule anomalie", "applicableSkills": "Domaines applicables", "moonDie": "Dé de lune", "none": "Aucune", @@ -79,16 +81,16 @@ }, "Faction": { "label": "Faction", - "score": "Score", + "relation": "Niveau de Relation", "custom": "Faction personnalisée…", - "pinkerton": "Pinkerton", - "police": "Police", + "pinkerton": "Agence Pinkerton", + "police": "Police secrète du duc", "okhrana": "Okhrana", - "lunanovatek": "LunaNovaTek", - "oto": "OTO", - "syndicats": "Syndicats", - "vorovskoymir": "Vorovskoymir", - "cour": "Cour" + "lunanovatek": "Luna NovaTek", + "oto": "Société théosophique OTO", + "syndicats": "Syndicats clandestins", + "vorovskoymir": "Vorovskoy Mir", + "cour": "Cour des merveilles" }, "Track": { "blessures": "Blessures", @@ -96,52 +98,66 @@ "spleen": "Spleen", "level": "Niveau", "currentMalus": "Malus actuel", + "blessuresTooltip": "Niveaux de blessures :\n1–2 : Anodin / Négligeable → aucun malus (1 min)\n3–4 : Dérisoire / Superficiel → −1 (10 min)\n5–6 : Léger / Modéré → −2 (30 min)\n7 : Grave → −3 (1 journée)\n8 : Dramatique → hors combat", + "spleenTooltip": "Le Spleen représente l'usure morale du protagoniste.\nLorsqu'il atteint son maximum, le protagoniste sombre dans la mélancolie et peut se retirer du scénario.", "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)", - "duration1min": "1 min", + "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 : " + "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)", - "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" + "initiative": "Initiative", + "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", + "targetLabel": "Cible", + "targetAuto": "Saisir manuellement", + "rangedMod": "Modificateur de tir", + "rangedModNone": "Aucun modificateur", + "rangedModAim": "Visée (dépense 1 tour) +2", + "rangedModMoving": "Cible mouvante −2", + "rangedModEngaged": "Engagé au contact −4", + "rangedModLongRange": "Longue portée −4" }, "Tab": { "main": "Principal", "competences": "Domaines", - "blessures": "Blessures", + "blessures": "Jauges", "factions": "Factions", "equipement": "Équipement", "biography": "Biographie", "description": "Description", - "technique": "Technique" + "technique": "Technique", + "biographie": "Biographie", + "aspects": "Aspects" }, "Roll": { "clickToRoll": "Cliquer pour lancer", + "resThresholdHint": "Cette case déclenche la Résistance de la spécialisation", "difficulty": "Difficulté", "modifier": "Modificateur", "nbDice": "Nombre de dés", @@ -169,10 +185,15 @@ "criticalFailureDesc": "Marge ≤ −5 — résultat désastreux !", "woundLevel": "Niveau de blessures", "threshold": "Seuil", + "opposition": "Test en opposition", + "oppositionDesc": "Le MJ détermine le résultat", + "oppositionResolved": "Le MJ détermine si le test est réussi ou non", + "oppositionVs": "En opposition", "baseDice": "2d8 de base", "formula": "Formule", "rollMoonDie": "Lancer le Dé de la Lune", "moonDieResult": "Dé de la Lune", + "armorMalus": "Malus armure équipée", "visibility": "Visibilité", "visibilityPublic": "Public", "visibilityGM": "MJ uniquement", @@ -185,17 +206,18 @@ "puiser": "Puiser dans ses ressources", "puiserDesc": "Ignore tous les malus — coche 1 case de Spleen", "usedPuiser": "Ressources puisées — malus ignorés, +1 Spleen", - "situationMod": "Mod. de situation", + "situationMod": "Modificateurs Situationnels", "resistanceTest": "Test de résistance", "resistanceClickToRoll": "Lancer un test de résistance", - "woundTaken": "Blessure cochée suite à l'échec" + "woundTaken": "Blessure cochée suite à l'échec", + "autoSuccess": "Réussite automatique" }, "Modifier": { - "evident": "Évident — Réussite automatique", - "malaise": "Malaisé (0)", - "difficile": "Difficile (−2)", - "tres": "Très difficile (−4)", - "extreme": "Extrêmement difficile (−6)", + "evident": "Évident — Réussite automatique", + "malaise": "Malaisé (0)", + "difficile": "Difficile (−2)", + "tres": "Très difficile (−4)", + "extreme": "Extrêmement difficile (−6)", "incroyable": "Incroyablement difficile (−8)" }, "Moon": { @@ -215,7 +237,14 @@ "contrecoup": "Contrecoup", "contrecoupFull": "Complication négative significative, perte de 2 Destin, ou −4 au prochain jet", "catastrophe": "Catastrophe", - "catastropheFull": "Échec catastrophique, perte d'1 utilisation d'Anomalie, ou gain d'1 Spleen" + "catastropheFull": "Échec catastrophique, perte d'1 utilisation d'Anomalie, ou gain d'1 Spleen", + "standalone": "Dé de la Lune", + "standaloneTitle": "Dé de la Lune", + "bonneFortune": "🟢 Bonne Fortune", + "mauvaiseFortune": "🔴 Mauvaise Fortune", + "chanceInterpret": "Chance", + "narrativeInterpret": "Narratif", + "quantiteHint": "Valeur" }, "Difficulty": { "unknown": "Aucun seuil", @@ -251,7 +280,8 @@ "newArmure": "Nouvelle armure", "noWeapons": "Aucune arme", "noArmures": "Aucune armure", - "noEquipments": "Aucun équipement" + "noEquipments": "Aucun équipement", + "noAspects": "Aucun aspect" }, "Equipment": { "autre": "Autre", @@ -263,6 +293,11 @@ "Sheet": { "editMode": "Mode édition", "playMode": "Mode jeu", + "character": "Fiche Protagoniste", + "npc": "Fiche PNJ", + "anomaly": "Fiche Anomalie", + "aspect": "Fiche Aspect", + "equipment": "Fiche Équipement", "weapon": "Fiche Arme", "armure": "Fiche Armure" }, @@ -301,34 +336,55 @@ "protection": "Protection", "protectionHint": "Réduit les blessures subies de ce montant", "malus": "Malus", - "malusHint": "Malus aux tests de Mobilité et Discrétion (ou Domaine Corps pour PNJ)" + "malusHint": "Malus aux tests de Mobilité et Discrétion (ou Domaine Corps pour PNJ)", + "malusLabel": "Malus armure", + "equipped": "Équipée", + "equippedHint": "Si cochée, le malus s'applique à tous les jets", + "equippedYes": "Oui — équipée", + "equippedNo": "Non — rangée", + "equip": "Équiper", + "unequip": "Retirer" }, "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", + "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", + "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" + "refAugmenterAspect": "Augmenter / Diminuer un Aspect", + "refAcquerirAttribut": "Acquérir ou augmenter un Attribut", + "refCoutAttributTotal": "= total des points × 10" + }, + "NPC": { + "typeStandard": "PNJ Standard", + "typeAntagoniste": "Antagoniste", + "type": "Type de PNJ", + "emprise": "Emprise", + "peril": "Péril", + "menace": "Menace", + "danger": "Danger", + "histoire": "Histoire", + "descriptionPhysique": "Description Physique", + "faction": "Faction", + "factionLabel": "Faction d'appartenance", + "factionNone": "Aucune faction" } } } \ 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 05c74f4..4f35780 100644 --- a/module/applications/sheets/base-actor-sheet.mjs +++ b/module/applications/sheets/base-actor-sheet.mjs @@ -29,6 +29,7 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou trackBox: CelestopolActorSheet.#onTrackBox, skillLevel: CelestopolActorSheet.#onSkillLevel, factionLevel: CelestopolActorSheet.#onFactionLevel, + toggleArmure: CelestopolActorSheet.#onToggleArmure, }, } @@ -173,12 +174,21 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou } /** Met à jour le score d'une faction par clic sur un point. */ + static async #onToggleArmure(_event, target) { + const uuid = target.closest('[data-item-uuid]')?.dataset.itemUuid + if (!uuid) return + const item = await fromUuid(uuid) + if (item?.type === "armure") await item.update({ "system.equipped": !item.system.equipped }) + } + static #onFactionLevel(_event, target) { if (!this.isEditable) return const factionId = target.dataset.faction - const index = parseInt(target.dataset.index) + const index = parseInt(target.dataset.index) // 0-8 + const newValue = index - 4 // -4 à +4 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) }) + // Cliquer sur le dot actif (sauf neutre) remet à 0 + const finalValue = (newValue === current && newValue !== 0) ? 0 : newValue + this.document.update({ [`system.factions.${factionId}.value`]: finalValue }) } } diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index 5f79d8f..0355bdf 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -17,6 +17,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses, depenseXp: CelestopolCharacterSheet.#onDepenseXp, supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog, + rollMoonDie: CelestopolCharacterSheet.#onRollMoonDie, }, } @@ -93,15 +94,46 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { case "factions": context.tab = context.tabs.factions + context.factionRows = Object.entries(SYSTEM.FACTIONS).map(([id, fDef]) => { + const val = this.document.system.factions[id]?.value ?? 0 + return { + id, + label: fDef.label, + value: val, + valueStr: val > 0 ? `+${val}` : `${val}`, + dots: Array.from({ length: 9 }, (_, i) => ({ + index: i, + filled: i <= val + 4, + type: i < 4 ? "neg" : i === 4 ? "neutral" : "pos", + })), + } + }) + context.factionCustom = ["perso1", "perso2"].map(id => { + const f = this.document.system.factions[id] + const val = f?.value ?? 0 + return { + id, + label: f?.label ?? "", + value: val, + valueStr: val > 0 ? `+${val}` : `${val}`, + dots: Array.from({ length: 9 }, (_, i) => ({ + index: i, + filled: i <= val + 4, + type: i < 4 ? "neg" : i === 4 ? "neutral" : "pos", + })), + } + }) break 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.enrichedDescriptionPhysique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + doc.system.descriptionPhysique, { relativeTo: this.document }) + context.enrichedDescriptionPsychologique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + doc.system.descriptionPsychologique, { relativeTo: this.document }) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML( - doc.system.notes, { async: true }) + doc.system.notes, { relativeTo: this.document }) break case "equipement": @@ -235,4 +267,10 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet { "system.xp.log": log, }) } + + /** Lance le Dé de la Lune de façon autonome depuis le header de la fiche. */ + static async #onRollMoonDie() { + const { CelestopolRoll } = await import("../../documents/roll.mjs") + await CelestopolRoll.rollMoonStandalone(this.document) + } } diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs index cf65591..337f4fc 100644 --- a/module/applications/sheets/npc-sheet.mjs +++ b/module/applications/sheets/npc-sheet.mjs @@ -5,24 +5,34 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet { /** @override */ static DEFAULT_OPTIONS = { classes: ["npc"], - position: { width: 760, height: 600 }, + position: { width: 780, height: 640 }, window: { contentClasses: ["npc-content"] }, + actions: { + createAspect: CelestopolNPCSheet.#onCreateAspect, + createWeapon: CelestopolNPCSheet.#onCreateWeapon, + createArmure: CelestopolNPCSheet.#onCreateArmure, + rollMoonDie: CelestopolNPCSheet.#onRollMoonDie, + }, } /** @override */ static PARTS = { - main: { template: "systems/fvtt-celestopol/templates/npc-main.hbs" }, - tabs: { template: "templates/generic/tab-navigation.hbs" }, - competences:{ template: "systems/fvtt-celestopol/templates/npc-competences.hbs" }, - blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.hbs" }, + main: { template: "systems/fvtt-celestopol/templates/npc-main.hbs" }, + tabs: { template: "templates/generic/tab-navigation.hbs" }, + competences: { template: "systems/fvtt-celestopol/templates/npc-competences.hbs" }, + blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.hbs" }, + equipement: { template: "systems/fvtt-celestopol/templates/npc-equipement.hbs" }, + biographie: { template: "systems/fvtt-celestopol/templates/npc-biographie.hbs" }, } tabGroups = { sheet: "competences" } #getTabs() { const tabs = { - competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" }, - blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" }, + competences: { id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" }, + blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" }, + equipement: { id: "equipement", group: "sheet", icon: "fa-solid fa-shield-halved",label: "CELESTOPOL.Tab.equipement" }, + biographie: { id: "biographie", group: "sheet", icon: "fa-solid fa-book-open", label: "CELESTOPOL.Tab.biographie" }, } for (const v of Object.values(tabs)) { v.active = this.tabGroups[v.group] === v.id @@ -33,12 +43,29 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet { /** @override */ async _prepareContext() { - const context = await super._prepareContext() - context.tabs = this.#getTabs() - context.stats = SYSTEM.STATS - context.skills = SYSTEM.SKILLS - context.anomalyTypes = SYSTEM.ANOMALY_TYPES - context.woundLevels = SYSTEM.WOUND_LEVELS + const context = await super._prepareContext() + context.tabs = this.#getTabs() + context.stats = SYSTEM.STATS + context.anomalyTypes = SYSTEM.ANOMALY_TYPES + context.woundLevels = SYSTEM.WOUND_LEVELS + context.npcTypes = SYSTEM.NPC_TYPES + context.factions = SYSTEM.FACTIONS + context.antagonisteStats = SYSTEM.ANTAGONISTE_STATS + + const sys = this.document.system + context.aspects = this.document.itemTypes.aspect ?? [] + context.weapons = this.document.itemTypes.weapon ?? [] + context.armures = this.document.itemTypes.armure ?? [] + context.armorMalus = sys.armorMalus ?? 0 + + // Label effectif de chaque domaine selon le type de PNJ + const isAntagoniste = sys.npcType === "antagoniste" + context.domainLabels = { + ame: isAntagoniste ? "CELESTOPOL.NPC.emprise" : "CELESTOPOL.Stat.ame", + corps: isAntagoniste ? "CELESTOPOL.NPC.peril" : "CELESTOPOL.Stat.corps", + coeur: isAntagoniste ? "CELESTOPOL.NPC.menace" : "CELESTOPOL.Stat.coeur", + esprit: isAntagoniste ? "CELESTOPOL.NPC.danger" : "CELESTOPOL.Stat.esprit", + } return context } @@ -48,11 +75,59 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet { switch (partId) { case "competences": context.tab = context.tabs.competences + // Enrichissement des aspects + context.enrichedAspects = await Promise.all( + (context.aspects ?? []).map(async a => ({ + item: a, + enrichedDesc: await foundry.applications.ux.TextEditor.implementation.enrichHTML( + a.system.description, { relativeTo: this.document } + ), + })) + ) break case "blessures": context.tab = context.tabs.blessures + context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + context.system.notes, { relativeTo: this.document } + ) + break + case "equipement": + context.tab = context.tabs.equipement + break + case "biographie": + context.tab = context.tabs.biographie + context.enrichedHistoire = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + context.system.histoire, { relativeTo: this.document } + ) + context.enrichedDescriptionPhysique = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + context.system.descriptionPhysique, { relativeTo: this.document } + ) break } return context } + + static async #onCreateAspect() { + await this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect", + }]) + } + + static async #onCreateWeapon() { + await this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon", + }]) + } + + static async #onCreateArmure() { + await this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("TYPES.Item.armure"), type: "armure", + }]) + } + + /** Lance le Dé de la Lune de façon autonome depuis le header de la fiche PNJ. */ + static async #onRollMoonDie() { + const { CelestopolRoll } = await import("../../documents/roll.mjs") + await CelestopolRoll.rollMoonStandalone(this.document) + } } diff --git a/module/config/system.mjs b/module/config/system.mjs index 53e4d9f..21739b4 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -171,6 +171,35 @@ export const WEAPON_RANGE_TYPES = { longue: { id: "longue", label: "CELESTOPOL.Weapon.rangeLongue" }, } +/** Types de PNJ : standard (domaines classiques) ou antagoniste (Emprise/Péril/Menace/Danger). */ +export const NPC_TYPES = { + standard: { id: "standard", label: "CELESTOPOL.NPC.typeStandard" }, + antagoniste: { id: "antagoniste", label: "CELESTOPOL.NPC.typeAntagoniste" }, +} + +/** + * Labels alternatifs des domaines pour les Antagonistes. + * Le domaine ame ↔ Emprise, corps ↔ Péril, coeur ↔ Menace, esprit ↔ Danger. + */ +export const ANTAGONISTE_STATS = { + ame: { id: "ame", label: "CELESTOPOL.NPC.emprise" }, + corps: { id: "corps", label: "CELESTOPOL.NPC.peril" }, + coeur: { id: "coeur", label: "CELESTOPOL.NPC.menace" }, + esprit: { id: "esprit", label: "CELESTOPOL.NPC.danger" }, +} + +/** + * Modificateurs de tir à distance (LdB p.XX). + * Affiché dans le dialogue de jet uniquement pour les armes de type "distance". + */ +export const RANGED_MODIFIERS = [ + { id: "none", value: 0, label: "CELESTOPOL.Combat.rangedModNone" }, + { id: "aim", value: +2, label: "CELESTOPOL.Combat.rangedModAim" }, + { id: "moving", value: -2, label: "CELESTOPOL.Combat.rangedModMoving" }, + { id: "engaged", value: -4, label: "CELESTOPOL.Combat.rangedModEngaged" }, + { id: "longRange", value: -4, label: "CELESTOPOL.Combat.rangedModLongRange" }, +] + export const SYSTEM = { id: SYSTEM_ID, ASCII, @@ -180,6 +209,8 @@ export const SYSTEM = { ANOMALY_TYPES, ANOMALY_DEFINITIONS, FACTIONS, + NPC_TYPES, + ANTAGONISTE_STATS, WOUND_LEVELS, DIFFICULTY_CHOICES, CONTEXT_MODIFIER_CHOICES, @@ -189,4 +220,5 @@ export const SYSTEM = { WEAPON_DAMAGE_TYPES, WEAPON_RANGE_TYPES, WEAPON_COMBAT_TYPES, + RANGED_MODIFIERS, } diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index fad50dc..f0fca3e 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -1,4 +1,5 @@ export { default as CelestopolActor } from "./actor.mjs" export { default as CelestopolItem } from "./item.mjs" export { default as CelestopolChatMessage } from "./chat-message.mjs" +export { default as CelestopolCombat } from "./combat.mjs" export { CelestopolRoll } from "./roll.mjs" diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 05f4c55..df33978 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,6 +1,22 @@ export default class CelestopolActor extends Actor { /** @override */ getRollData() { - return this.toObject(false).system + // Inclure les valeurs dérivées (initiative, résistances…) calculées par prepareDerivedData + return { ...this.toObject(false).system, initiative: this.system.initiative ?? 0 } + } + + /** + * Override de l'initiative : valeur déterministe (pas de dé). + * Personnage : 4 + Mobilité + Inspiration + * PNJ : Corps.res + * @override + */ + async rollInitiative() { + if (!game.combat) return null + const combatant = game.combat.combatants.find(c => c.actorId === this.id) + if (!combatant) return null + const initiative = this.system.initiative ?? 0 + await combatant.update({ initiative }) + return combatant } } diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs new file mode 100644 index 0000000..76eb1ed --- /dev/null +++ b/module/documents/combat.mjs @@ -0,0 +1,53 @@ +const SYSTEM_ID = "fvtt-celestopol" + +export default class CelestopolCombat extends Combat { + /** @override — Initiative déterministe, message stylé maison */ + async rollInitiative(ids, { updateTurn = true } = {}) { + ids = typeof ids === "string" ? [ids] : ids + const combatants = ids.map(id => this.combatants.get(id)).filter(Boolean) + if (!combatants.length) return this + + const updates = [] + for (const combatant of combatants) { + const actor = combatant.actor + if (!actor) continue + const value = actor.system.initiative ?? 0 + updates.push({ _id: combatant.id, initiative: value }) + await CelestopolCombat._postInitiativeMessage(combatant, actor, value) + } + + if (updates.length) await this.updateEmbeddedDocuments("Combatant", updates) + if (updateTurn && this.turn !== null) await this.update({ turn: this.turn }) + return this + } + + static async _postInitiativeMessage(combatant, actor, value) { + const sys = actor.system + let detail + if (actor.type === "character") { + const mob = sys.stats?.corps?.mobilite?.value ?? 0 + const insp = sys.stats?.coeur?.inspiration?.value ?? 0 + detail = `4 + ${mob} (${game.i18n.localize("CELESTOPOL.Skill.mobilite")}) + ${insp} (${game.i18n.localize("CELESTOPOL.Skill.inspiration")})` + } else { + const corps = sys.stats?.corps?.res ?? value + detail = `${game.i18n.localize("CELESTOPOL.Stat.corps")} : ${corps}` + } + + const content = await renderTemplate( + `systems/${SYSTEM_ID}/templates/chat-initiative.hbs`, + { + actorName: combatant.name ?? actor.name, + actorImg: actor.img, + value, + detail, + } + ) + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + content, + style: CONST.CHAT_MESSAGE_STYLES.OTHER, + flags: { [SYSTEM_ID]: { type: "initiative" } }, + }) + } +} diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index f7da217..0f11a28 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -35,18 +35,21 @@ export class CelestopolRoll extends Roll { * @returns {Promise} */ static async prompt(options = {}) { - const woundMalus = options.woundMalus ?? 0 - const skillValue = options.skillValue ?? 0 - const woundLevelId = options.woundLevel ?? 0 - const destGaugeFull = options.destGaugeFull ?? false - const fortuneValue = options.fortuneValue ?? 0 - const isResistance = options.isResistance ?? false - const isCombat = options.isCombat ?? false + const woundMalus = options.woundMalus ?? 0 + const armorMalus = options.armorMalus ?? 0 + const skillValue = options.skillValue ?? 0 + const woundLevelId = options.woundLevel ?? 0 + const destGaugeFull = options.destGaugeFull ?? false + 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" - const woundLabel = woundLevelId > 0 + const weaponType = options.weaponType ?? "melee" + const weaponName = options.weaponName ?? null + const weaponDegats = options.weaponDegats ?? "0" + const availableTargets = options.availableTargets ?? [] + const isRangedAttack = isCombat && !isRangedDefense && weaponType === "distance" + const woundLabel = woundLevelId > 0 ? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "") : null @@ -63,6 +66,11 @@ export class CelestopolRoll extends Roll { const v = i - 8 return { value: v, label: v > 0 ? `+${v}` : `${v}` } }) + const rangedModChoices = SYSTEM.RANGED_MODIFIERS.map(m => ({ + id: m.id, + value: m.value, + label: game.i18n.localize(m.label), + })) const dialogContext = { actorName: options.actorName, @@ -71,20 +79,22 @@ export class CelestopolRoll extends Roll { skillValue, woundMalus, woundLabel, - difficultyChoices: SYSTEM.DIFFICULTY_CHOICES, - defaultDifficulty: options.difficulty ?? "normal", - destGaugeFull, - defaultRollMoonDie: options.rollMoonDie ?? false, - isResistance, isCombat, isRangedDefense, + isRangedAttack, weaponType, weaponName, weaponDegats, modifierChoices, aspectChoices, situationChoices, + rangedModChoices, + availableTargets, fortuneValue, + armorMalus, + destGaugeFull, + defaultRollMoonDie: options.rollMoonDie ?? false, + isResistance, } const content = await foundry.applications.handlebars.renderTemplate( @@ -104,8 +114,40 @@ export class CelestopolRoll extends Roll { const wrap = dialog.element.querySelector('.roll-dialog-content') if (!wrap) return - function hasMalus(mod, asp, sit) { - return woundMalus < 0 || mod < 0 || asp < 0 || sit < 0 + // Sélection de cible PNJ : masque le champ Corps PNJ (valeur cachée) + const targetSelect = wrap.querySelector('#targetSelect') + const corpsPnjRow = wrap.querySelector('#corps-pnj-row') + const targetConfirmedRow = wrap.querySelector('#target-confirmed-row') + const targetConfirmedName = wrap.querySelector('#target-confirmed-name') + + function applyTargetSelection() { + if (!targetSelect) return + const selectedOption = targetSelect.options[targetSelect.selectedIndex] + const val = parseFloat(targetSelect.value) + const corpsPnjInput = wrap.querySelector('#corpsPnj') + if (targetSelect.value && !isNaN(val)) { + // Cible sélectionnée : masquer la valeur, afficher le nom + if (corpsPnjRow) corpsPnjRow.style.display = 'none' + if (targetConfirmedRow) targetConfirmedRow.style.display = '' + if (targetConfirmedName) targetConfirmedName.textContent = selectedOption?.text ?? '' + if (corpsPnjInput) { + corpsPnjInput.value = val + corpsPnjInput.dispatchEvent(new Event('input')) + } + } else { + // Saisie manuelle + if (corpsPnjRow) corpsPnjRow.style.display = '' + if (targetConfirmedRow) targetConfirmedRow.style.display = 'none' + } + } + + if (targetSelect) { + targetSelect.addEventListener('change', applyTargetSelection) + applyTargetSelection() + } + + function hasMalus(mod, asp, sit, ranged) { + return woundMalus < 0 || armorMalus < 0 || mod < 0 || asp < 0 || sit < 0 || ranged < 0 } function update() { @@ -114,6 +156,7 @@ export class CelestopolRoll extends Roll { const modifier = autoSucc ? 0 : (parseInt(rawMod ?? 0) || 0) const aspectMod = parseInt(wrap.querySelector('#aspectModifier')?.value ?? 0) || 0 const situMod = parseInt(wrap.querySelector('#situationMod')?.value ?? 0) || 0 + const rangedMod = parseInt(wrap.querySelector('#rangedMod')?.value ?? 0) || 0 const useDestin = wrap.querySelector('#useDestin')?.checked const useFort = wrap.querySelector('#useFortune')?.checked const puiser = wrap.querySelector('#puiserRessources')?.checked @@ -122,7 +165,7 @@ export class CelestopolRoll extends Roll { // En résistance : pas de "Puiser" possible const puiserRow = wrap.querySelector('#puiser-row') if (puiserRow) { - if (!isResistance && hasMalus(modifier, aspectMod, situMod)) { + if (!isResistance && hasMalus(modifier, aspectMod, situMod, rangedMod)) { puiserRow.style.display = '' } else { puiserRow.style.display = 'none' @@ -135,7 +178,9 @@ export class CelestopolRoll extends Roll { const effMod = puiser ? Math.max(0, modifier) : modifier const effAspect = puiser ? Math.max(0, aspectMod) : aspectMod const effSit = puiser ? Math.max(0, situMod) : situMod - const totalMod = skillValue + effWound + effMod + effAspect + effSit + const effArmor = puiser ? 0 : armorMalus + const effRanged = puiser ? Math.max(0, rangedMod) : rangedMod + const totalMod = skillValue + effWound + effMod + effAspect + effSit + effArmor + effRanged let formula if (autoSucc) { @@ -153,7 +198,7 @@ export class CelestopolRoll extends Roll { if (previewEl) previewEl.textContent = formula } - wrap.querySelectorAll('#modifier, #aspectModifier, #situationMod, #useDestin, #useFortune, #puiserRessources, #corpsPnj') + wrap.querySelectorAll('#modifier, #aspectModifier, #situationMod, #rangedMod, #useDestin, #useFortune, #puiserRessources, #corpsPnj') .forEach(el => { el.addEventListener('change', update) el.addEventListener('input', update) @@ -179,16 +224,18 @@ export class CelestopolRoll extends Roll { if (!rollContext) return null - // En combat : Corps PNJ = seuil direct (pas le sélect difficulté) + // En combat : Corps PNJ = seuil direct ; sinon seuil fixe = 11 const corpsPnj = isCombat ? (parseInt(rollContext.corpsPnj ?? 7) || 7) : null - const difficulty = isCombat ? "combat" : (rollContext.difficulty ?? "normal") + const difficulty = isCombat ? "combat" : "standard" const diffConfig = isCombat ? { value: corpsPnj, label: "CELESTOPOL.Combat.corpsPnj" } - : (SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal) + : { value: 11, label: "CELESTOPOL.Roll.threshold" } const autoSuccess = rollContext.modifier === "auto" const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0) const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0 const situationMod = parseInt(rollContext.situationMod ?? 0) || 0 + const rangedMod = isRangedAttack ? (parseInt(rollContext.rangedMod ?? 0) || 0) : 0 + const isOpposition = !isCombat && !isResistance && (rollContext.isOpposition === true || rollContext.isOpposition === "true") const useDestin = destGaugeFull && (rollContext.useDestin === true || rollContext.useDestin === "true") const useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true") const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "true" @@ -200,13 +247,15 @@ export class CelestopolRoll extends Roll { // Puiser dans ses ressources → ignorer tous les malus const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus + const effectiveArmorMalus = effectivePuiser ? 0 : armorMalus const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod const effectiveSituationMod = effectivePuiser ? Math.max(0, situationMod) : situationMod + const effectiveRangedMod = effectivePuiser ? Math.max(0, rangedMod) : rangedMod // Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8 const nbDice = (!isResistance && useDestin) ? 3 : 2 - const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier + effectiveSituationMod + const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier + effectiveSituationMod + effectiveArmorMalus + effectiveRangedMod const formula = (!isResistance && useFortune) ? buildFormula(1, totalModifier + 8) : buildFormula(nbDice, totalModifier) @@ -232,11 +281,13 @@ export class CelestopolRoll extends Roll { woundMalus: effectiveWoundMalus, autoSuccess, isResistance, + isOpposition, isCombat, isRangedDefense, weaponType, weaponName, weaponDegats, + rangedMod: effectiveRangedMod, useDestin: !isResistance && useDestin, useFortune: !isResistance && useFortune, puiserRessources: effectivePuiser, @@ -294,9 +345,11 @@ export class CelestopolRoll extends Roll { } } - // Mémoriser les préférences - updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie - updateData["system.prefs.difficulty"] = difficulty + // Mémoriser les préférences (protagonistes uniquement — le modèle NPC n'a pas de champ prefs) + if (actor.type === "character") { + updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie + updateData["system.prefs.difficulty"] = difficulty + } await actor.update(updateData) } @@ -306,11 +359,8 @@ export class CelestopolRoll extends Roll { /** * Détermine succès/échec selon la marge (total − seuil). - * - Marge ≥ 5 → succès critique - * - Marge > 0 → succès - * - Marge = 0 → succès (ou égalité en combat) - * - Marge ≤ −5 → échec critique - * - Marge < 0 → échec + * Seuil : 11 pour les tests normaux, Corps PNJ pour le combat. + * Pas de succès/échec critique — seul le Dé de la Lune produit des résultats exceptionnels. */ computeResult() { if (this.options.autoSuccess) { @@ -318,9 +368,15 @@ export class CelestopolRoll extends Roll { this.options.margin = null return } + // En test d'opposition : pas de résultat calculé — le MJ décide + if (this.options.isOpposition) { + this.options.resultType = "opposition" + this.options.margin = null + return + } const threshold = this.options.isCombat ? (this.options.difficultyValue ?? 0) - : (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0) + : 11 if (threshold === 0) { this.options.resultType = "unknown" this.options.margin = null @@ -330,10 +386,11 @@ export class CelestopolRoll extends Roll { this.options.margin = margin if (this.options.isCombat && margin === 0) { this.options.resultType = "tie" - } else if (margin >= 5) this.options.resultType = "critical-success" - else if (margin >= 0) this.options.resultType = "success" - else if (margin <= -5) this.options.resultType = "critical-failure" - else this.options.resultType = "failure" + } else if (margin >= 0) { + this.options.resultType = "success" + } else { + this.options.resultType = "failure" + } } /** @override */ @@ -350,7 +407,7 @@ export class CelestopolRoll extends Roll { const diceSum = diceResults.reduce((a, b) => a + b, 0) const threshold = this.options.isCombat ? (this.options.difficultyValue ?? 0) - : (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0) + : 11 const margin = this.options.margin const woundMalus = this.options.woundMalus ?? 0 const skillValue = this.options.skillValue ?? 0 @@ -365,18 +422,21 @@ export class CelestopolRoll extends Roll { const moonResultType = this.options.moonResultType ?? null const resultClassMap = { - "critical-success": "critical-success", - "success": "success", - "tie": "tie", - "failure": "failure", - "critical-failure": "critical-failure", - "unknown": "", + "success": "success", + "tie": "tie", + "failure": "failure", + "opposition": "opposition", + "unknown": "", } - // Libellé de difficulté : en combat, afficher "Corps PNJ : N" + const isOpposition = this.options.isOpposition ?? false + + // Libellé de difficulté : en combat "Corps PNJ : N", en opposition "vs ?", sinon "Seuil : 11" const difficultyLabel = this.options.isCombat ? `${game.i18n.localize("CELESTOPOL.Combat.corpsPnj")} : ${threshold}` - : game.i18n.localize(SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.label ?? "") + : isOpposition + ? `${game.i18n.localize("CELESTOPOL.Roll.oppositionVs")}` + : `${game.i18n.localize("CELESTOPOL.Roll.threshold")} : 11` return { cssClass: [SYSTEM.id, "dice-roll"].join(" "), @@ -394,11 +454,10 @@ export class CelestopolRoll extends Roll { isSuccess: this.isSuccess, isFailure: this.isFailure, isTie: this.isTie, - isCriticalSuccess: this.isCriticalSuccess, - isCriticalFailure: this.isCriticalFailure, + isOpposition, difficulty: this.options.difficulty, difficultyLabel, - difficultyValue: threshold, + difficultyValue: isOpposition ? null : threshold, margin, marginAbs: margin !== null ? Math.abs(margin) : null, marginAbove: margin !== null && margin >= 0, @@ -419,6 +478,8 @@ export class CelestopolRoll extends Roll { weaponType: this.options.weaponType ?? null, isRangedDefense: this.options.isRangedDefense ?? false, woundTaken: this.options.woundTaken ?? null, + situationMod: this.options.situationMod ?? 0, + rangedMod: this.options.rangedMod ?? 0, // Dé de lune hasMoonDie: moonDieResult !== null, moonDieResult, @@ -442,4 +503,44 @@ export class CelestopolRoll extends Roll { : `${skillLocalized}` return super.toMessage({ flavor, ...messageData }, { rollMode }) } + + /** + * Lance le dé de la Lune de façon autonome (hors test de spécialisation). + * Affiche un carte de chat avec le résultat narratif ET l'interprétation chance. + * @param {Actor|null} actor Acteur initiateur (pour le speaker du message) + */ + static async rollMoonStandalone(actor = null) { + const roll = await new Roll("1d8").evaluate() + const result = roll.total + const face = SYSTEM.MOON_DIE_FACES[result] ?? null + const resultType = face ? SYSTEM.MOON_RESULT_TYPES[face.result] ?? null : null + const isGoodFortune = result <= 4 + + const templateData = { + result, + moonFaceSymbol: face?.symbol ?? "", + moonFaceLabel: face ? game.i18n.localize(face.label) : "", + moonResultLabel: resultType ? game.i18n.localize(resultType.label) : "", + moonResultDesc: resultType ? game.i18n.localize(resultType.desc) : "", + moonResultClass: resultType?.cssClass ?? "", + isGoodFortune, + actorName: actor?.name ?? null, + } + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-celestopol/templates/moon-standalone.hbs", + templateData + ) + + const speaker = actor + ? ChatMessage.getSpeaker({ actor }) + : { alias: game.i18n.localize("CELESTOPOL.Moon.standalone") } + + await ChatMessage.create({ + content, + speaker, + rolls: [roll], + style: CONST.CHAT_MESSAGE_STYLES?.ROLL ?? 5, + }) + } } diff --git a/module/models/character.mjs b/module/models/character.mjs index 32618cd..a47f28a 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -74,9 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel vision: persoAttrField(), }) - // Factions — score entier direct (0-9) + // Factions — niveau de relation -4 (hostile) à +4 (allié), 0 = neutre const factionField = () => new fields.SchemaField({ - value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 9 }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: -4, max: 4 }), }) schema.factions = new fields.SchemaField({ pinkerton: factionField(), @@ -89,11 +89,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel cour: factionField(), perso1: new fields.SchemaField({ label: new fields.StringField({ required: true, nullable: false, initial: "" }), - value: new fields.NumberField({ ...reqInt, initial: 0 }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: -4, max: 4 }), }), perso2: new fields.SchemaField({ label: new fields.StringField({ required: true, nullable: false, initial: "" }), - value: new fields.NumberField({ ...reqInt, initial: 0 }), + value: new fields.NumberField({ ...reqInt, initial: 0, min: -4, max: 4 }), }), }) @@ -114,8 +114,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel }) // Description & notes - schema.description = new fields.HTMLField({ required: true, textSearch: true }) - schema.notes = new fields.HTMLField({ required: true, textSearch: true }) + schema.descriptionPhysique = new fields.HTMLField({ required: true, textSearch: true }) + schema.descriptionPsychologique = new fields.HTMLField({ required: true, textSearch: true }) + schema.notes = new fields.HTMLField({ required: true, textSearch: true }) // Données biographiques schema.biodata = new fields.SchemaField({ @@ -152,6 +153,20 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel // XP dépensée = somme des montants du log this.xp.depense = this.xp.log.reduce((sum, entry) => sum + entry.montant, 0) + + // Malus d'armure(s) équipée(s) + this.armorMalus = this.getArmorMalus() + } + + /** + * Retourne le malus total des armures équipées portées par le protagoniste. + * @returns {number} + */ + getArmorMalus() { + if (!this.parent) return 0 + return -(this.parent.itemTypes.armure + .filter(a => a.system.equipped && a.system.malus > 0) + .reduce((sum, a) => sum + a.system.malus, 0)) } /** @@ -183,6 +198,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel skillLabel: skill.label, skillValue: skill.value, woundMalus: this.getWoundMalus(), + armorMalus: this.getArmorMalus(), woundLevel: this.blessures.lvl, difficulty: this.prefs.difficulty, rollMoonDie: this.prefs.rollMoonDie ?? false, @@ -213,6 +229,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel skillLabel: "CELESTOPOL.Roll.resistanceTest", skillValue: statData.res, woundMalus: this.getWoundMalus(), + armorMalus: this.getArmorMalus(), woundLevel: this.blessures.lvl, isResistance: true, rollMoonDie: false, @@ -222,6 +239,38 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel }) } + /** + * Collecte les tokens PNJs disponibles comme cibles de combat. + * Priorise le combat tracker, sinon les tokens ciblés par l'utilisateur. + * @returns {Array<{id:string, name:string, corps:number}>} + */ + _getCombatTargets() { + const toEntry = actor => ({ + id: actor.id, + name: actor.name, + corps: actor.system.stats?.corps?.res ?? 0, + }) + // Priorité 1 : PNJs dans le combat actif + if (game.combat?.active) { + const list = game.combat.combatants + .filter(c => c.actor?.type === "npc" && c.actorId !== this.parent.id) + .map(c => toEntry(c.actor)) + if (list.length) return list + } + // Priorité 2 : Tokens ciblés par le joueur + const targeted = [...(game.user?.targets ?? [])] + .filter(t => t.actor?.type === "npc") + .map(t => toEntry(t.actor)) + if (targeted.length) return targeted + // Priorité 3 : Tous les tokens NPC de la scène active + if (canvas?.tokens?.placeables) { + return canvas.tokens.placeables + .filter(t => t.actor?.type === "npc" && t.actor.id !== this.parent.id) + .map(t => toEntry(t.actor)) + } + return [] + } + /** * Lance une attaque avec une arme. * Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur. @@ -238,24 +287,26 @@ 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, - isRangedDefense: false, - 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(), + armorMalus: this.getArmorMalus(), + 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, + availableTargets: this._getCombatTargets(), }) } @@ -274,24 +325,26 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel 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", + 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(), + armorMalus: this.getArmorMalus(), + 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", + availableTargets: this._getCombatTargets(), }) } } diff --git a/module/models/items.mjs b/module/models/items.mjs index 8fcbd0e..180e261 100644 --- a/module/models/items.mjs +++ b/module/models/items.mjs @@ -84,6 +84,7 @@ export class CelestopolArmure extends foundry.abstract.TypeDataModel { return { protection: new fields.NumberField({ ...reqInt, initial: 1, min: 1, max: 2 }), malus: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 2 }), + equipped: new fields.BooleanField({ initial: false }), description: new fields.HTMLField({ required: true, textSearch: true }), } } diff --git a/module/models/npc.mjs b/module/models/npc.mjs index 67478ee..ce3c51c 100644 --- a/module/models/npc.mjs +++ b/module/models/npc.mjs @@ -6,7 +6,10 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel { const reqInt = { required: true, nullable: false, integer: true } const schema = {} - schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" }) + schema.npcType = new fields.StringField({ required: true, nullable: false, initial: "standard", + choices: Object.keys(SYSTEM.NPC_TYPES) }) + schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" }) + schema.faction = new fields.StringField({ required: true, nullable: false, initial: "" }) schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 }) schema.anomaly = new fields.SchemaField({ @@ -15,43 +18,27 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel { value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), }) - const skillField = (label) => new fields.SchemaField({ - label: new fields.StringField({ required: true, initial: label }), - value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + // PNJs : 4 domaines uniquement (pas de sous-compétences) + const domainField = (statId) => new fields.SchemaField({ + label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }), + res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), + actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), }) - const statField = (statId) => { - const skills = SYSTEM.SKILLS[statId] - const skillSchema = {} - for (const [key, skill] of Object.entries(skills)) { - skillSchema[key] = skillField(skill.label) - } - return new fields.SchemaField({ - label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }), - res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), - actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), // res + wound malus - ...skillSchema, - }) - } - schema.stats = new fields.SchemaField({ - ame: statField("ame"), - corps: statField("corps"), - coeur: statField("coeur"), - esprit: statField("esprit"), + ame: domainField("ame"), + corps: domainField("corps"), + coeur: domainField("coeur"), + esprit: domainField("esprit"), }) schema.blessures = new fields.SchemaField({ lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), }) - schema.prefs = new fields.SchemaField({ - rollMoonDie: new fields.BooleanField({ required: true, initial: false }), - difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }), - }) - - schema.description = new fields.HTMLField({ required: true, textSearch: true }) - schema.notes = new fields.HTMLField({ required: true, textSearch: true }) + schema.histoire = new fields.HTMLField({ required: true, textSearch: true }) + schema.descriptionPhysique = new fields.HTMLField({ required: true, textSearch: true }) + schema.notes = new fields.HTMLField({ required: true, textSearch: true }) return schema } @@ -61,11 +48,12 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel { prepareDerivedData() { super.prepareDerivedData() const malus = this.getWoundMalus() - // Initiative PNJ : valeur du Domaine Corps + // Initiative PNJ : valeur du Domaine Corps (avec malus blessures) this.initiative = Math.max(0, this.stats.corps.res + malus) for (const stat of Object.values(this.stats)) { stat.actuel = Math.max(0, stat.res + malus) } + this.armorMalus = this.getArmorMalus() } getWoundMalus() { @@ -73,22 +61,43 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel { return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0 } - async roll(statId, skillId) { + /** Somme des malus des armures équipées (valeur négative ou 0). */ + getArmorMalus() { + const armures = this.parent?.itemTypes?.armure ?? [] + return armures + .filter(a => a.system.equipped) + .reduce((sum, a) => sum + (a.system.malus ? -Math.abs(a.system.malus) : 0), 0) + } + + /** + * Lance un jet sur un domaine (Âme/Corps/Cœur/Esprit). + * Le label affiché tient compte du type de PNJ (standard vs antagoniste). + */ + async roll(statId) { const { CelestopolRoll } = await import("../documents/roll.mjs") - const skill = this.stats[statId][skillId] - if (!skill) return null + const statData = this.stats[statId] + if (!statData) return null + + const isAntagoniste = this.npcType === "antagoniste" + const skillLabel = isAntagoniste + ? SYSTEM.ANTAGONISTE_STATS[statId]?.label + : SYSTEM.STATS[statId]?.label return CelestopolRoll.prompt({ actorId: this.parent.id, actorName: this.parent.name, actorImage: this.parent.img, statId, - skillId, - skillLabel: skill.label, - skillValue: skill.value, + skillLabel, + skillValue: statData.res, woundMalus: this.getWoundMalus(), - difficulty: this.prefs.difficulty, - rollMoonDie: this.prefs.rollMoonDie ?? false, + armorMalus: this.getArmorMalus(), + woundLevel: this.blessures.lvl, }) } + + /** Alias pour compatibilité avec le handler _onRoll (clic sans skillId). */ + async rollResistance(statId) { + return this.roll(statId) + } } diff --git a/packs-system/anomalies/000017.ldb b/packs-system/anomalies/000017.ldb new file mode 100644 index 0000000..363cf2e Binary files /dev/null and b/packs-system/anomalies/000017.ldb differ diff --git a/packs-system/anomalies/000019.log b/packs-system/anomalies/000019.log new file mode 100644 index 0000000..81cfb05 Binary files /dev/null and b/packs-system/anomalies/000019.log differ diff --git a/packs-system/anomalies/000035.log b/packs-system/anomalies/000035.log deleted file mode 100644 index e69de29..0000000 diff --git a/packs-system/anomalies/000037.ldb b/packs-system/anomalies/000037.ldb deleted file mode 100644 index a906f9c..0000000 Binary files a/packs-system/anomalies/000037.ldb and /dev/null differ diff --git a/packs-system/anomalies/CURRENT b/packs-system/anomalies/CURRENT index a56825e..e417a51 100644 --- a/packs-system/anomalies/CURRENT +++ b/packs-system/anomalies/CURRENT @@ -1 +1 @@ -MANIFEST-000033 +MANIFEST-000018 diff --git a/packs-system/anomalies/LOG b/packs-system/anomalies/LOG index 921bbf2..ad3c26f 100644 --- a/packs-system/anomalies/LOG +++ b/packs-system/anomalies/LOG @@ -1,15 +1,3 @@ -2026/03/31-17:30:25.623001 7ff9fd9ff6c0 Recovering log #30 -2026/03/31-17:30:25.660522 7ff9fd9ff6c0 Delete type=3 #28 -2026/03/31-17:30:25.660594 7ff9fd9ff6c0 Delete type=0 #30 -2026/03/31-17:32:44.418768 7ff7477ef6c0 Level-0 table #36: started -2026/03/31-17:32:44.422584 7ff7477ef6c0 Level-0 table #36: 3529 bytes OK -2026/03/31-17:32:44.428754 7ff7477ef6c0 Delete type=0 #34 -2026/03/31-17:32:44.451369 7ff7477ef6c0 Manual compaction at level-0 from '!items!anomCommMorts001' @ 72057594037927935 : 1 .. '!items!null' @ 0 : 0; will stop at (end) -2026/03/31-17:32:44.451426 7ff7477ef6c0 Manual compaction at level-1 from '!items!anomCommMorts001' @ 72057594037927935 : 1 .. '!items!null' @ 0 : 0; will stop at '!items!null' @ 33 : 1 -2026/03/31-17:32:44.451435 7ff7477ef6c0 Compacting 1@1 + 1@2 files -2026/03/31-17:32:44.454717 7ff7477ef6c0 Generated table #37@1: 9 keys, 6638 bytes -2026/03/31-17:32:44.454749 7ff7477ef6c0 Compacted 1@1 + 1@2 files => 6638 bytes -2026/03/31-17:32:44.461425 7ff7477ef6c0 compacted to: files[ 0 0 1 0 0 0 0 ] -2026/03/31-17:32:44.461630 7ff7477ef6c0 Delete type=2 #32 -2026/03/31-17:32:44.461862 7ff7477ef6c0 Delete type=2 #36 -2026/03/31-17:32:44.487417 7ff7477ef6c0 Manual compaction at level-1 from '!items!null' @ 33 : 1 .. '!items!null' @ 0 : 0; will stop at (end) +2026/04/06-17:46:52.532955 7f67ebfff6c0 Recovering log #15 +2026/04/06-17:46:52.543005 7f67ebfff6c0 Delete type=3 #13 +2026/04/06-17:46:52.543081 7f67ebfff6c0 Delete type=0 #15 diff --git a/packs-system/anomalies/LOG.old b/packs-system/anomalies/LOG.old index 3a8833c..00a7d2b 100644 --- a/packs-system/anomalies/LOG.old +++ b/packs-system/anomalies/LOG.old @@ -1,15 +1,15 @@ -2026/03/31-14:51:45.342017 7ff9fd1fe6c0 Recovering log #25 -2026/03/31-14:51:45.425193 7ff9fd1fe6c0 Delete type=3 #23 -2026/03/31-14:51:45.425262 7ff9fd1fe6c0 Delete type=0 #25 -2026/03/31-16:02:12.525970 7ff7477ef6c0 Level-0 table #31: started -2026/03/31-16:02:12.529284 7ff7477ef6c0 Level-0 table #31: 3529 bytes OK -2026/03/31-16:02:12.536284 7ff7477ef6c0 Delete type=0 #29 -2026/03/31-16:02:12.536504 7ff7477ef6c0 Manual compaction at level-0 from '!items!anomCommMorts001' @ 72057594037927935 : 1 .. '!items!null' @ 0 : 0; will stop at (end) -2026/03/31-16:02:12.547124 7ff7477ef6c0 Manual compaction at level-1 from '!items!anomCommMorts001' @ 72057594037927935 : 1 .. '!items!null' @ 0 : 0; will stop at '!items!null' @ 29 : 1 -2026/03/31-16:02:12.547133 7ff7477ef6c0 Compacting 1@1 + 1@2 files -2026/03/31-16:02:12.550304 7ff7477ef6c0 Generated table #32@1: 9 keys, 6638 bytes -2026/03/31-16:02:12.550331 7ff7477ef6c0 Compacted 1@1 + 1@2 files => 6638 bytes -2026/03/31-16:02:12.557451 7ff7477ef6c0 compacted to: files[ 0 0 1 0 0 0 0 ] -2026/03/31-16:02:12.557568 7ff7477ef6c0 Delete type=2 #27 -2026/03/31-16:02:12.557744 7ff7477ef6c0 Delete type=2 #31 -2026/03/31-16:02:12.557862 7ff7477ef6c0 Manual compaction at level-1 from '!items!null' @ 29 : 1 .. '!items!null' @ 0 : 0; will stop at (end) +2026/04/05-21:02:44.634018 7f8249dff6c0 Recovering log #10 +2026/04/05-21:02:44.729398 7f8249dff6c0 Delete type=3 #8 +2026/04/05-21:02:44.729470 7f8249dff6c0 Delete type=0 #10 +2026/04/06-00:09:38.933436 7f82177fe6c0 Level-0 table #16: started +2026/04/06-00:09:38.937122 7f82177fe6c0 Level-0 table #16: 3525 bytes OK +2026/04/06-00:09:38.943462 7f82177fe6c0 Delete type=0 #14 +2026/04/06-00:09:38.943723 7f82177fe6c0 Manual compaction at level-0 from '!items!anomCommMorts001' @ 72057594037927935 : 1 .. '!items!null' @ 0 : 0; will stop at (end) +2026/04/06-00:09:38.966124 7f82177fe6c0 Manual compaction at level-1 from '!items!anomCommMorts001' @ 72057594037927935 : 1 .. '!items!null' @ 0 : 0; will stop at '!items!null' @ 17 : 1 +2026/04/06-00:09:38.966141 7f82177fe6c0 Compacting 1@1 + 1@2 files +2026/04/06-00:09:38.969869 7f82177fe6c0 Generated table #17@1: 9 keys, 6617 bytes +2026/04/06-00:09:38.969906 7f82177fe6c0 Compacted 1@1 + 1@2 files => 6617 bytes +2026/04/06-00:09:38.976148 7f82177fe6c0 compacted to: files[ 0 0 1 0 0 0 0 ] +2026/04/06-00:09:38.976266 7f82177fe6c0 Delete type=2 #12 +2026/04/06-00:09:38.976457 7f82177fe6c0 Delete type=2 #16 +2026/04/06-00:09:38.987710 7f82177fe6c0 Manual compaction at level-1 from '!items!null' @ 17 : 1 .. '!items!null' @ 0 : 0; will stop at (end) diff --git a/packs-system/anomalies/MANIFEST-000018 b/packs-system/anomalies/MANIFEST-000018 new file mode 100644 index 0000000..3405437 Binary files /dev/null and b/packs-system/anomalies/MANIFEST-000018 differ diff --git a/packs-system/anomalies/MANIFEST-000033 b/packs-system/anomalies/MANIFEST-000033 deleted file mode 100644 index fdaf4ff..0000000 Binary files a/packs-system/anomalies/MANIFEST-000033 and /dev/null differ diff --git a/styles/character.less b/styles/character.less index 1dd3ae0..4a15acb 100644 --- a/styles/character.less +++ b/styles/character.less @@ -54,6 +54,18 @@ } } + // Badge malus armure équipée dans le header + .armor-malus-badge { + border-color: #b84a2e; + .armor-malus-value { + color: #e06040; + font-family: var(--cel-font-title); + font-size: 1.05em; + font-weight: bold; + } + label { color: #e06040; opacity: 0.8; } + } + // Stats × Domaines grid .stats-grid { display: grid; @@ -100,6 +112,47 @@ text-align: center; } } + + .stat-res-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px 3px 6px; + border: 1px solid var(--cel-orange); + border-radius: 4px; + background: rgba(224, 123, 0, 0.08); + font-size: 0.78em; + cursor: default; + + .res-die-icon { + font-size: 1.1em; + color: var(--cel-orange); + opacity: 0.85; + } + + .res-label { + color: var(--cel-orange-light); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .res-value { + font-size: 1.3em; + font-weight: bold; + color: var(--cel-orange); + min-width: 18px; + text-align: center; + } + + &.rollable { + cursor: pointer; + transition: background 0.15s, border-color 0.15s; + &:hover { + background: rgba(224, 123, 0, 0.22); + border-color: var(--cel-orange-light); + } + } + } } .skills-list { @@ -146,11 +199,36 @@ border-radius: 1px; background: rgba(255,255,255,0.3); vertical-align: middle; + position: relative; transition: background 0.1s, border-color 0.1s; &.filled { background: var(--cel-orange); border-color: var(--cel-border); } + &.res-threshold { + border: 2px solid var(--cel-orange); + background: rgba(224, 123, 0, 0.2); + // Petit indicateur orange sous le dot + &::after { + content: ''; + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%); + width: 4px; + height: 4px; + background: var(--cel-orange); + border-radius: 50%; + } + &.filled { + background: var(--cel-orange); + border: 2px solid #fff; + box-shadow: 0 0 0 1.5px var(--cel-orange); + &::after { + background: var(--cel-orange); + } + } + } &[data-action] { cursor: pointer; } } } @@ -201,12 +279,30 @@ font-family: var(--cel-font-title); font-weight: bold; text-transform: uppercase; - font-size: 0.9em; + font-size: 1.1em; + letter-spacing: 0.04em; + display: flex; + align-items: center; + gap: 5px; } - .track-title-destin { + .track-help { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 50%; + border: 1px solid currentColor; + font-size: 0.65em; + font-family: var(--cel-font-body); + font-weight: bold; + text-transform: none; + letter-spacing: 0; cursor: help; - border-bottom: 1px dashed currentColor; - text-decoration: none; + opacity: 0.7; + transition: opacity 0.15s; + flex-shrink: 0; + &:hover { opacity: 1; } } } @@ -270,7 +366,7 @@ background-image: url("../assets/ui/fond_cadrille.jpg"); background-blend-mode: soft-light; color: var(--cel-orange); - th { padding: 5px 8px; font-family: var(--cel-font-title); letter-spacing: 0.06em; } + th { padding: 5px 8px; font-family: var(--cel-font-title); font-size: 1.05em; letter-spacing: 0.06em; text-transform: uppercase; } } .faction-row { @@ -299,8 +395,14 @@ 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; } + // Dot neutre (centre, index 4) + &.neutral { border-color: #888; } + &.neutral.filled { background: #aaa; border-color: #888; } + // Dots positifs (alliés) → or + &.pos.filled { background: var(--cel-orange); border-color: var(--cel-orange); } + // Dots négatifs (hostiles) → rouge terracotta + &.neg.filled { background: #b84a2e; border-color: #b84a2e; } } .faction-count { @@ -345,6 +447,12 @@ .item-row { .cel-item-row(); + &.is-equipped { + background: rgba(12, 76, 12, 0.12); + border-left: 3px solid var(--cel-green); + padding-left: 5px; + } + .item-tag { font-size: 0.75em; padding: 1px 7px; @@ -355,6 +463,12 @@ white-space: nowrap; &.malus { background: rgba(192,68,68,0.1); border-color: rgba(192,68,68,0.35); color: #922; } } + + .equip-toggle { + color: var(--cel-border); + &.equipped { color: var(--cel-green); } + &:hover { color: var(--cel-orange); } + } } .equip-empty { @@ -489,34 +603,50 @@ margin-top: 6px; summary { font-size: 0.78em; - color: var(--cel-border); + color: var(--cel-orange-light); cursor: pointer; letter-spacing: 0.03em; text-transform: uppercase; user-select: none; - &:hover { color: var(--cel-green); } + &:hover { color: var(--cel-orange); } } .xp-ref-table { width: 100%; border-collapse: collapse; - font-size: 0.78em; - margin-top: 5px; - opacity: 0.85; + font-size: 0.82em; + margin-top: 6px; + thead tr { + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + color: var(--cel-orange); + } th { - color: var(--cel-border); + font-family: var(--cel-font-title); + font-size: 0.9em; + letter-spacing: 0.05em; text-transform: uppercase; - font-size: 0.85em; - letter-spacing: 0.03em; - padding: 2px 6px; - border-bottom: 1px solid rgba(196,154,26,0.25); + padding: 4px 8px; text-align: left; + border-bottom: 2px solid var(--cel-orange); } td { - padding: 2px 6px; - border-bottom: 1px solid rgba(196,154,26,0.1); - color: var(--cel-text-dark, #3a2a0a); + padding: 4px 8px; + color: var(--cel-text, #2a1a00); + border-bottom: 1px solid rgba(196,154,26,0.25); + } + tbody tr { + &:nth-child(odd) { background: rgba(255,248,230,0.7); } + &:nth-child(even) { background: rgba(240,228,195,0.5); } + &:last-child td { border-bottom: none; } + } + td:last-child { + font-weight: bold; + color: var(--cel-orange); + text-align: center; + width: 60px; } } } diff --git a/styles/global.less b/styles/global.less index 6ca10f8..39b7265 100644 --- a/styles/global.less +++ b/styles/global.less @@ -186,6 +186,42 @@ } } } + + // Boutons d'action du header (toggle mode, dé de lune, etc.) + .header-buttons { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 6px; + padding-left: 4px; + + a { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 3px; + color: var(--cel-orange-light); + font-size: 1em; + cursor: pointer; + transition: background 0.15s, color 0.15s; + text-decoration: none; + + &:hover { + background: rgba(196,154,26,0.18); + color: var(--cel-orange); + } + } + + .moon-standalone-btn { + font-size: 1.3em; + line-height: 1; + width: 30px; + height: 30px; + } + } } // ─── Tabs ──────────────────────────────────────────────────────────────── diff --git a/styles/items.less b/styles/items.less index a62286d..6a6f621 100644 --- a/styles/items.less +++ b/styles/items.less @@ -387,12 +387,13 @@ } // Armure-specific - &.armure { + &.armure { .armure-stats { display: flex; gap: 14px; justify-content: center; margin: 12px 0; + flex-wrap: wrap; } .armure-stat-box { display: flex; @@ -405,12 +406,37 @@ min-width: 110px; label { font-size: 0.72em; text-transform: uppercase; color: var(--cel-orange-light); letter-spacing: 0.05em; } .armure-stat-value { - input, span { + input[type="number"], span { font-family: var(--cel-font-title); font-size: 1.8em; font-weight: bold; color: var(--cel-orange); text-align: center; background: transparent; border: none; width: 40px; } } .armure-stat-hint { font-size: 0.7em; color: var(--cel-cream); font-style: italic; text-align: center; margin-top: 4px; } } + .equipped-box { + border-color: var(--cel-green); + .equipped-switch { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + input[type="checkbox"] { display: none; } + .switch-label { + font-family: var(--cel-font-ui); + font-size: 0.9em; + color: var(--cel-border); + padding: 4px 12px; + border: 1px solid var(--cel-border); + border-radius: 20px; + transition: all 0.2s; + white-space: nowrap; + &.on { + color: var(--cel-green-light); + border-color: var(--cel-green); + background: rgba(12,76,12,0.3); + } + } + } + } } } diff --git a/styles/npc.less b/styles/npc.less index 1c51ac2..639cb03 100644 --- a/styles/npc.less +++ b/styles/npc.less @@ -4,75 +4,250 @@ .fvtt-celestopol.npc { - .stats-grid { + // ── Sélecteur type PNJ (en-tête) ──────────────────────────────────────── + .npc-type-row { + margin: 3px 0; + + .npc-type-select { + background: rgba(12,76,12,0.15); + border: 1px solid var(--cel-border); + color: var(--cel-orange); + border-radius: 3px; + padding: 2px 6px; + font-size: 0.85em; + } + + .npc-type-badge { + font-family: var(--cel-font-title); + font-size: 0.8em; + letter-spacing: 0.05em; + text-transform: uppercase; + border-radius: 3px; + padding: 2px 8px; + + &.antagoniste { + background: rgba(120, 30, 30, 0.25); + border: 1px solid rgba(200, 60, 60, 0.5); + color: #e06060; + } + } + } + + // ── Grille 2×2 des domaines ────────────────────────────────────────────── + .npc-domains-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 8px 0; - .stat-block { + .npc-domain-block { border: 1px solid var(--cel-border); border-radius: 4px; overflow: hidden; + } - .stat-header { - background: var(--cel-green); - background-image: url("../assets/ui/fond_cadrille.jpg"); - background-blend-mode: soft-light; - color: var(--cel-orange); + .npc-domain-header { + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + color: var(--cel-orange); + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + min-height: 60px; + + .npc-domain-labels { display: flex; - justify-content: space-between; - align-items: center; - padding: 5px 8px; - border-bottom: 1px solid rgba(196,154,26,0.4); + flex-direction: column; + gap: 2px; - .stat-name { + .domain-label-primary { font-family: var(--cel-font-title); font-weight: bold; + font-size: 1.1em; text-transform: uppercase; - font-size: 0.9em; + letter-spacing: 0.06em; + color: var(--cel-orange); } - .stat-res { - display: flex; - align-items: center; - gap: 4px; - font-size: 0.8em; - - label { color: var(--cel-orange-light); } - .stat-res-value { font-weight: bold; color: var(--cel-orange); } - .stat-actuel { - font-size: 0.9em; - color: rgba(255,200,0,0.7); - font-style: italic; - } - input[type="number"] { width: 30px; .cel-input-std(); } + .domain-label-secondary { + font-size: 0.75em; + color: rgba(220,170,80,0.7); + font-style: italic; + text-transform: uppercase; + letter-spacing: 0.04em; } } - .skills-list { - background: var(--cel-cream); + .npc-domain-value-wrap { + display: flex; + align-items: center; - .skill-row { + // Mode édition : input nombre + input.domain-value-input { + width: 40px; + .cel-input-std(); + font-size: 1.2em; + text-align: center; + font-family: var(--cel-font-title); + } + + // Mode jeu : bouton rollable avec dé + .npc-domain-roll-btn { display: flex; align-items: center; - justify-content: space-between; - padding: 3px 8px; - border-bottom: 1px solid rgba(122,92,32,0.18); - font-size: 0.85em; + gap: 5px; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.15s, box-shadow 0.15s; - &:nth-child(even) { background: var(--cel-cream-dark); } + &:hover { + background: rgba(224,123,0,0.2); + box-shadow: 0 0 6px rgba(224,123,0,0.4); + .domain-die-icon { color: var(--cel-orange); } + } - &.rollable { .cel-rollable(); } + .domain-die-icon { + font-size: 1.2em; + color: rgba(220,170,80,0.7); + } - .skill-name { flex: 1; } - .skill-value { font-weight: bold; color: var(--cel-orange); min-width: 24px; text-align: center; } - .skill-value-input { width: 36px; .cel-input-std(); text-align: center; } + .domain-value { + font-family: var(--cel-font-title); + font-size: 1.4em; + font-weight: bold; + color: var(--cel-orange); + min-width: 20px; + text-align: center; + } + + .domain-value-base { + font-size: 0.75em; + color: rgba(220,170,80,0.6); + font-style: italic; + } } } } } + // ── Section Aspects ────────────────────────────────────────────────────── + .npc-aspects-section { + margin-top: 12px; + border: 1px solid var(--cel-border); + border-radius: 4px; + overflow: hidden; + + .section-header { + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + color: var(--cel-orange); + display: flex; + align-items: center; + gap: 8px; + padding: 5px 10px; + font-family: var(--cel-font-title); + font-weight: bold; + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: 0.06em; + border-bottom: 1px solid rgba(196,154,26,0.4); + + a { color: var(--cel-orange-light); margin-left: auto; } + } + + .aspect-row { + display: flex; + align-items: center; + padding: 4px 10px; + border-bottom: 1px solid rgba(122,92,32,0.18); + background: var(--cel-cream); + font-size: 0.9em; + gap: 8px; + + &:nth-child(even) { background: var(--cel-cream-dark); } + + .item-name { flex: 1; } + .aspect-value { + font-family: var(--cel-font-title); + font-weight: bold; + min-width: 28px; + text-align: center; + &.positive { color: #2a8a2a; } + &.negative { color: #c03030; } + } + } + } + + // ── Onglet Biographie ──────────────────────────────────────────────────── + .bio-section { + margin-bottom: 12px; + + .section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 10px; + background: var(--cel-green); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + color: var(--cel-orange); + font-family: var(--cel-font-title); + font-weight: bold; + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: 0.06em; + border-radius: 4px 4px 0 0; + border: 1px solid var(--cel-border); + border-bottom: none; + } + + .enriched-html { + font-size: 0.9em; + line-height: 1.6; + } + } + + .faction-section { + .faction-display { + padding: 8px 12px; + background: var(--cel-cream); + border: 1px solid var(--cel-border); + border-top: none; + border-radius: 0 0 4px 4px; + + .faction-name { + font-family: var(--cel-font-title); + color: var(--cel-orange); + font-size: 0.95em; + } + + .faction-none { + font-style: italic; + color: rgba(122,92,32,0.5); + font-size: 0.85em; + } + } + + .faction-select-row { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + background: var(--cel-cream); + border: 1px solid var(--cel-border); + border-top: none; + border-radius: 0 0 4px 4px; + + label { font-size: 0.85em; color: var(--cel-brown); } + select { flex: 1; .cel-input-std(); } + } + } + + // ── Pistes (Blessures) ─────────────────────────────────────────────────── .track-section { border: 1px solid var(--cel-border); border-radius: 4px; diff --git a/styles/roll.less b/styles/roll.less index ab2721a..5d7709d 100644 --- a/styles/roll.less +++ b/styles/roll.less @@ -857,4 +857,345 @@ padding: 2px 4px; } } + + .form-target-row { + background: rgba(12, 76, 12, 0.12); + border: 1px solid rgba(196, 154, 26, 0.3); + border-radius: 4px; + padding: 4px 8px; + + label { + color: var(--cel-orange, #e07b00); + font-weight: bold; + font-size: 0.85em; + display: flex; + align-items: center; + gap: 5px; + + i { color: #e07b00; } + } + + select { + flex: 1; + background: rgba(0, 0, 0, 0.25); + color: #f0e0c0; + border: 1px solid rgba(196, 154, 26, 0.5); + border-radius: 3px; + padding: 2px 4px; + font-size: 0.85em; + max-width: 200px; + } + } + + .form-target-confirmed { + background: rgba(12, 76, 12, 0.2); + border: 1px solid rgba(196, 154, 26, 0.5); + border-radius: 4px; + padding: 5px 10px; + + .target-confirmed-badge { + display: flex; + align-items: center; + gap: 6px; + color: var(--cel-orange, #e07b00); + font-size: 0.88em; + font-style: italic; + + i { opacity: 0.8; } + } + } + + .form-ranged-mod { + background: rgba(60, 20, 0, 0.12); + border: 1px solid rgba(200, 100, 60, 0.35); + border-radius: 4px; + padding: 4px 8px; + + label { + color: #e08060; + font-size: 0.85em; + font-weight: bold; + display: flex; + align-items: center; + gap: 5px; + + i { color: #e08060; } + } + + select { + flex: 1; + background: rgba(0, 0, 0, 0.25); + color: #f0e0c0; + border: 1px solid rgba(200, 100, 60, 0.4); + border-radius: 3px; + padding: 2px 4px; + font-size: 0.85em; + } + } + + .form-threshold-fixed { + .threshold-value { + font-size: 1.2em; + font-weight: bold; + font-family: var(--cel-font-title, "CopaseticNF", serif); + color: var(--cel-orange, #e07b00); + padding: 2px 10px; + background: rgba(0,0,0,0.2); + border: 1px solid rgba(224,123,0,0.4); + border-radius: 4px; + } + } + + .form-opposition-row { + padding: 5px 8px; + border: 1px solid rgba(180, 140, 60, 0.35); + border-radius: 5px; + background: rgba(60, 30, 0, 0.15); + + .opposition-toggle { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + + input[type="checkbox"] { flex-shrink: 0; } + + .opposition-icon { + font-size: 1.2em; + color: var(--cel-orange, #e07b00); + } + + .opposition-text { + display: flex; + flex-direction: column; + gap: 1px; + + .opposition-main { + font-weight: bold; + font-size: 0.9em; + font-family: var(--cel-font-title, "CopaseticNF", serif); + color: var(--cel-orange, #e07b00); + } + + .opposition-sub { + font-size: 0.75em; + opacity: 0.7; + font-style: italic; + } + } + } + } +} + +// Bandeau opposition dans le chat +.fvtt-celestopol .dice-roll .roll-result-banner.opposition { + background: linear-gradient(135deg, rgba(60, 60, 80, 0.8), rgba(40, 40, 60, 0.9)); + border-color: rgba(150, 140, 200, 0.5); + color: #c8c0e0; +} + +// ── Dé de la Lune — Carte autonome ────────────────────────────────────────── + +.celestopol-roll.moon-standalone-card { + padding: 8px 12px; + background: var(--cel-parchment, #f5eed8); + border: 1px solid var(--cel-border, rgba(196,154,26,0.4)); + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 6px; + + .moon-standalone-header { + display: flex; + align-items: baseline; + gap: 6px; + border-bottom: 1px solid rgba(196,154,26,0.3); + padding-bottom: 4px; + + .moon-standalone-title { + font-family: var(--cel-font-title); + font-size: 1em; + font-weight: bold; + color: var(--cel-green-dark, #0c4c0c); + text-transform: uppercase; + letter-spacing: 0.06em; + } + + .moon-standalone-actor { + font-size: 0.85em; + color: var(--cel-text, #333); + font-style: italic; + } + } + + .moon-standalone-main { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 8px; + background: rgba(0,0,0,0.04); + border-radius: 3px; + border-left: 3px solid transparent; + + .moon-standalone-symbol { + font-size: 2em; + line-height: 1; + flex-shrink: 0; + } + + .moon-standalone-info { + display: flex; + flex-direction: column; + gap: 2px; + } + + .moon-standalone-phase { + font-family: var(--cel-font-title); + font-size: 0.95em; + font-weight: bold; + color: var(--cel-text, #333); + } + + .moon-standalone-value { + font-size: 0.8em; + color: var(--cel-text-light, #666); + font-style: italic; + } + + &.moon-triomphe { border-left-color: var(--cel-green, #0c4c0c); } + &.moon-brio { border-left-color: var(--cel-border, #7a5c20); } + &.moon-contrecoup { border-left-color: #c07800; } + &.moon-catastrophe{ border-left-color: #8b1e2e; } + } + + .moon-interpret-row { + display: flex; + align-items: center; + gap: 8px; + + .moon-interpret-label { + font-size: 0.72em; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #888; + white-space: nowrap; + } + + .moon-fortune { + font-size: 0.85em; + font-weight: bold; + border-radius: 3px; + padding: 1px 8px; + + &.bonne-fortune { + background: rgba(12, 76, 12, 0.12); + color: var(--cel-green, #0c4c0c); + border: 1px solid rgba(12,76,12,0.3); + } + + &.mauvaise-fortune { + background: rgba(139, 30, 46, 0.1); + color: #8b1e2e; + border: 1px solid rgba(139,30,46,0.3); + } + } + } + + // Réutilise les styles .moon-die-result existants pour le bloc narratif + .moon-die-result { + margin-top: 2px; + padding: 6px 8px; + } +} + +// ── Message d'initiative ────────────────────────────────────────────────────── +.celestopol.chat-initiative { + border: 1px solid var(--cel-orange, #e07b00); + border-radius: 4px; + overflow: hidden; + font-family: var(--cel-font-body, "Palatino Linotype", serif); + + .roll-header { + display: flex; + align-items: center; + gap: 8px; + background-color: var(--cel-green, #0c4c0c); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + padding: 6px 8px; + border-bottom: 2px solid var(--cel-orange, #e07b00); + + .actor-img { + width: 40px; + height: 40px; + object-fit: cover; + border: 1px solid var(--cel-orange, #e07b00); + border-radius: 2px; + flex-shrink: 0; + } + + .roll-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 1px; + + .actor-name { + font-family: var(--cel-font-title, "CopaseticNF", serif); + color: var(--cel-orange, #e07b00); + font-weight: bold; + letter-spacing: 0.05em; + font-size: 0.92em; + } + .skill-info { + color: var(--cel-cream, #f0e8d4); + font-size: 0.77em; + font-style: italic; + } + } + } + + .initiative-banner { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px 8px 8px; + background-color: var(--cel-green, #0c4c0c); + background-image: url("../assets/ui/fond_cadrille.jpg"); + background-blend-mode: soft-light; + color: #ffd870; + font-family: var(--cel-font-title, "CopaseticNF", serif); + text-transform: uppercase; + letter-spacing: 0.1em; + text-shadow: 0 1px 3px rgba(0,0,0,0.7); + + .initiative-score-wrap { + display: flex; + align-items: center; + gap: 6px; + line-height: 1; + } + + .initiative-icon { + font-size: 1.1em; + opacity: 0.9; + font-style: normal; + } + + .initiative-score { + font-size: 2.4em; + font-weight: bold; + line-height: 1; + } + + .initiative-detail { + font-size: 0.65em; + opacity: 0.75; + font-style: italic; + font-family: var(--cel-font-body, serif); + text-transform: none; + letter-spacing: 0.03em; + margin-top: 4px; + } + } } diff --git a/templates/armure.hbs b/templates/armure.hbs index c99cc15..136e43d 100644 --- a/templates/armure.hbs +++ b/templates/armure.hbs @@ -10,6 +10,17 @@
+
+ +
+ +
+
diff --git a/templates/character-biography.hbs b/templates/character-biography.hbs index f5c7f2f..7e0e188 100644 --- a/templates/character-biography.hbs +++ b/templates/character-biography.hbs @@ -1,9 +1,15 @@
- {{!-- Description / Biographie --}} + {{!-- Description Physique --}}
-
{{localize "CELESTOPOL.Actor.description"}}
- {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} +
{{localize "CELESTOPOL.Actor.descriptionPhysique"}}
+ {{formInput systemFields.descriptionPhysique enriched=enrichedDescriptionPhysique value=system.descriptionPhysique name="system.descriptionPhysique" toggled=true}} +
+ + {{!-- Description Psychologique --}} +
+
{{localize "CELESTOPOL.Actor.descriptionPsychologique"}}
+ {{formInput systemFields.descriptionPsychologique enriched=enrichedDescriptionPsychologique value=system.descriptionPsychologique name="system.descriptionPsychologique" toggled=true}}
{{!-- Notes --}} diff --git a/templates/character-blessures.hbs b/templates/character-blessures.hbs index 71311cf..7c4a717 100644 --- a/templates/character-blessures.hbs +++ b/templates/character-blessures.hbs @@ -2,7 +2,10 @@ {{!-- Blessures --}}
- {{localize "CELESTOPOL.Track.blessures"}} + + {{localize "CELESTOPOL.Track.blessures"}} + ? + {{localize "CELESTOPOL.Track.currentMalus"}} : {{lookup @root.woundLevels system.blessures.lvl 'malus'}} @@ -24,8 +27,9 @@ {{!-- Destin --}}
- {{localize "CELESTOPOL.Track.destin"}} + {{localize "CELESTOPOL.Track.destin"}} + ? +
{{#each (range 8) as |lvl|}} @@ -42,7 +46,10 @@ {{!-- Spleen --}}
- {{localize "CELESTOPOL.Track.spleen"}} + + {{localize "CELESTOPOL.Track.spleen"}} + ? +
{{#each (range 8) as |lvl|}} diff --git a/templates/character-competences.hbs b/templates/character-competences.hbs index 759787c..e2ac67c 100644 --- a/templates/character-competences.hbs +++ b/templates/character-competences.hbs @@ -5,10 +5,11 @@
{{localize stat.label}} -
- - {{lookup (lookup ../system.stats statId) 'res'}} + + {{localize "CELESTOPOL.Stat.res"}} + {{lookup (lookup ../system.stats statId) 'res'}}
@@ -19,8 +20,9 @@
{{#each (range 8) as |lvl|}} - + {{/each}}
@@ -33,7 +35,8 @@
{{#each (range 8) as |lvl|}} - + {{/each}}
diff --git a/templates/character-equipement.hbs b/templates/character-equipement.hbs index 272653e..49bca1a 100644 --- a/templates/character-equipement.hbs +++ b/templates/character-equipement.hbs @@ -41,12 +41,17 @@ {{/if}}
{{#each armures as |item|}} -
+
{{item.name}} {{item.system.protection}} {{#if item.system.malus}}−{{item.system.malus}} {{localize "CELESTOPOL.Armure.malus"}}{{/if}}
+ + + {{#if ../isEditMode}}{{/if}}
diff --git a/templates/character-factions.hbs b/templates/character-factions.hbs index ae725bf..e955c7b 100644 --- a/templates/character-factions.hbs +++ b/templates/character-factions.hbs @@ -3,72 +3,53 @@ {{localize "CELESTOPOL.Faction.label"}} - {{localize "CELESTOPOL.Faction.score"}} + {{localize "CELESTOPOL.Faction.relation"}} - {{#each factions as |faction factionId|}} - + {{!-- Factions standard --}} + {{#each factionRows as |faction|}} + {{localize faction.label}}
- {{#each (range 9) as |level|}} - + {{#each faction.dots as |dot|}} + {{/each}}
- {{lookup @root.system.factions factionId 'value'}} + {{faction.valueStr}}
{{/each}} {{!-- Factions personnalisées --}} - + {{#each factionCustom as |faction|}} + - {{#if isEditMode}} - {{else}} - {{#if system.factions.perso1.label}}{{system.factions.perso1.label}}{{else}}—{{/if}} + {{#if faction.label}}{{faction.label}}{{else}}—{{/if}} {{/if}}
- {{#each (range 9) as |level|}} - + {{#each faction.dots as |dot|}} + {{/each}}
- {{system.factions.perso1.value}} -
- - - - - {{#if isEditMode}} - - {{else}} - {{#if system.factions.perso2.label}}{{system.factions.perso2.label}}{{else}}—{{/if}} - {{/if}} - - -
-
- {{#each (range 9) as |level|}} - - {{/each}} -
- {{system.factions.perso2.value}} + {{faction.valueStr}}
+ {{/each}}
diff --git a/templates/character-main.hbs b/templates/character-main.hbs index 10a2718..3f41356 100644 --- a/templates/character-main.hbs +++ b/templates/character-main.hbs @@ -82,10 +82,19 @@
{{/if}} {{/with}} + {{#if system.armorMalus}} +
+ + {{system.armorMalus}} +
+ {{/if}}
+ + 🌙 + diff --git a/templates/chat-initiative.hbs b/templates/chat-initiative.hbs new file mode 100644 index 0000000..1a48b20 --- /dev/null +++ b/templates/chat-initiative.hbs @@ -0,0 +1,27 @@ +
+ + {{!-- En-tête : acteur --}} +
+ {{#if actorImg}} + {{actorName}} + {{/if}} +
+ {{actorName}} + + ⚡ {{localize "CELESTOPOL.Combat.initiative"}} + +
+
+ + {{!-- Bandeau initiative --}} +
+
+ + {{value}} +
+ {{#if detail}} + {{detail}} + {{/if}} +
+ +
diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs index 518ddf4..a9ea288 100644 --- a/templates/chat-message.hbs +++ b/templates/chat-message.hbs @@ -71,7 +71,8 @@ {{total}}
- {{!-- Seuil et marge --}} + {{!-- Seuil et marge (masqué en opposition) --}} + {{#unless isOpposition}}
vs @@ -84,6 +85,7 @@ {{/if}}
+ {{/unless}} {{!-- Infos bonus (Destin, Fortune, Aspect) --}} {{#if useDestin}} @@ -119,22 +121,14 @@ {{#if autoSuccess}} {{localize "CELESTOPOL.Roll.autoSuccess"}} + {{else if isOpposition}} + + {{localize "CELESTOPOL.Roll.opposition"}} + {{localize "CELESTOPOL.Roll.oppositionResolved"}} {{else if isTie}} {{localize "CELESTOPOL.Combat.tie"}} {{localize "CELESTOPOL.Combat.tieDesc"}} - {{else if isCriticalSuccess}} - ✦✦ - {{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}} {{else if isSuccess}} {{localize "CELESTOPOL.Roll.success"}} @@ -145,20 +139,6 @@ {{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 if isRangedDefense}} - {{localize "CELESTOPOL.Combat.rangedDefenseFailure"}} - {{else}} - {{localize "CELESTOPOL.Combat.distanceNoWound"}} - {{/if}} - {{else}} - {{localize "CELESTOPOL.Roll.criticalFailureDesc"}} - {{/if}} {{else if isFailure}} {{localize "CELESTOPOL.Roll.failure"}} diff --git a/templates/moon-standalone.hbs b/templates/moon-standalone.hbs new file mode 100644 index 0000000..ab2ed53 --- /dev/null +++ b/templates/moon-standalone.hbs @@ -0,0 +1,39 @@ +
+ + {{!-- En-tête --}} +
+ {{localize "CELESTOPOL.Moon.standaloneTitle"}} + {{#if actorName}} + — {{actorName}} + {{/if}} +
+ + {{!-- Résultat principal : phase + valeur --}} +
+ {{moonFaceSymbol}} +
+ {{moonFaceLabel}} + {{localize "CELESTOPOL.Moon.quantiteHint"}} : {{result}} +
+
+ + {{!-- Interprétation Chance --}} +
+ {{localize "CELESTOPOL.Moon.chanceInterpret"}} + {{#if isGoodFortune}} + {{localize "CELESTOPOL.Moon.bonneFortune"}} + {{else}} + {{localize "CELESTOPOL.Moon.mauvaiseFortune"}} + {{/if}} +
+ + {{!-- Interprétation Narrative --}} +
+
+ {{localize "CELESTOPOL.Moon.narrativeInterpret"}} + {{moonResultLabel}} + {{moonResultDesc}} +
+
+ +
diff --git a/templates/npc-biographie.hbs b/templates/npc-biographie.hbs new file mode 100644 index 0000000..858f93d --- /dev/null +++ b/templates/npc-biographie.hbs @@ -0,0 +1,48 @@ +
+ + {{!-- Faction --}} +
+
+ + {{localize "CELESTOPOL.NPC.faction"}} +
+ {{#if isEditMode}} +
+ + +
+ {{else}} +
+ {{#if system.faction}} + {{localize (lookup (lookup factions system.faction) 'label')}} + {{else}} + {{localize "CELESTOPOL.NPC.factionNone"}} + {{/if}} +
+ {{/if}} +
+ + {{!-- Histoire --}} +
+
+ + {{localize "CELESTOPOL.NPC.histoire"}} +
+ {{formInput systemFields.histoire enriched=enrichedHistoire value=system.histoire name="system.histoire" toggled=true}} +
+ + {{!-- Description Physique --}} +
+
+ + {{localize "CELESTOPOL.NPC.descriptionPhysique"}} +
+ {{formInput systemFields.descriptionPhysique enriched=enrichedDescriptionPhysique value=system.descriptionPhysique name="system.descriptionPhysique" toggled=true}} +
+ +
diff --git a/templates/npc-blessures.hbs b/templates/npc-blessures.hbs index 5d2c1f7..02a40e3 100644 --- a/templates/npc-blessures.hbs +++ b/templates/npc-blessures.hbs @@ -1,7 +1,13 @@
- {{localize "CELESTOPOL.Track.blessures"}} + + {{localize "CELESTOPOL.Track.blessures"}} + ? + + {{localize "CELESTOPOL.Track.currentMalus"}} : + {{lookup @root.woundLevels system.blessures.lvl 'malus'}} +
{{#each (range 8) as |lvl|}} @@ -17,8 +23,9 @@
- {{!-- Description --}} -
- {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} + {{!-- Notes (libres) --}} +
+
{{localize "CELESTOPOL.Actor.notes"}}
+ {{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
diff --git a/templates/npc-competences.hbs b/templates/npc-competences.hbs index 4bf8fd9..3da435e 100644 --- a/templates/npc-competences.hbs +++ b/templates/npc-competences.hbs @@ -1,37 +1,65 @@
-
+ + {{!-- Grille des 4 domaines --}} +
{{#each stats as |stat statId|}} -
-
- {{localize stat.label}} -
- - {{#if ../isEditMode}} - +
+
+ {{!-- Double label : Âme / Emprise --}} +
+ {{localize (lookup ../domainLabels statId)}} + {{#if (eq ../system.npcType "antagoniste")}} + {{localize (lookup ../stats statId 'label')}} {{else}} - - {{lookup ../system.stats statId 'actuel'}} / {{lookup ../system.stats statId 'res'}} - + {{localize (lookup ../antagonisteStats statId 'label')}} {{/if}}
-
-
- {{#each (lookup ../skills statId) as |skill skillId|}} -
- {{localize skill.label}} + {{!-- Valeur du domaine --}} +
{{#if ../isEditMode}} - + {{else}} - {{lookup (lookup ../system.stats statId) skillId 'value'}} +
+ + {{lookup ../system.stats statId 'actuel'}} + /{{lookup ../system.stats statId 'res'}} +
{{/if}}
- {{/each}}
{{/each}}
+ + {{!-- Aspects --}} + {{#if (or aspects.length isEditMode)}} +
+
+ + {{localize "CELESTOPOL.Tab.aspects"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each aspects as |item|}} +
+ {{item.name}} + + {{#if (gt item.system.valeur 0)}}+{{/if}}{{item.system.valeur}} + +
+ + {{#if ../isEditMode}}{{/if}} +
+
+ {{else}} + {{#unless ../isEditMode}} +

{{localize "CELESTOPOL.Item.noAspects"}}

+ {{/unless}} + {{/each}} +
+ {{/if}} +
diff --git a/templates/npc-equipement.hbs b/templates/npc-equipement.hbs new file mode 100644 index 0000000..4c17461 --- /dev/null +++ b/templates/npc-equipement.hbs @@ -0,0 +1,58 @@ +
+ + {{!-- ── Armes ─────────────────────────────────────────────────────────── --}} +
+
+ + {{localize "CELESTOPOL.Item.weapons"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each weapons as |item|}} +
+ + {{item.name}} + {{#if (eq item.system.type "melee")}}{{localize "CELESTOPOL.Weapon.typeMelee"}}{{else}}{{localize "CELESTOPOL.Weapon.typeDistance"}}{{/if}} + {{localize "CELESTOPOL.Weapon.degats"}} {{item.system.degats}} +
+ + {{#if ../isEditMode}}{{/if}} +
+
+ {{else}} +

{{localize "CELESTOPOL.Item.noWeapons"}}

+ {{/each}} +
+ + {{!-- ── Armures ───────────────────────────────────────────────────────── --}} +
+
+ + {{localize "CELESTOPOL.Item.armures"}} + {{#if isEditMode}} + + {{/if}} +
+ {{#each armures as |item|}} +
+ + {{item.name}} + {{item.system.protection}} + {{#if item.system.malus}}−{{item.system.malus}} {{localize "CELESTOPOL.Armure.malus"}}{{/if}} +
+ + + + + {{#if ../isEditMode}}{{/if}} +
+
+ {{else}} +

{{localize "CELESTOPOL.Item.noArmures"}}

+ {{/each}} +
+ +
diff --git a/templates/npc-main.hbs b/templates/npc-main.hbs index 4839985..55bef1f 100644 --- a/templates/npc-main.hbs +++ b/templates/npc-main.hbs @@ -19,6 +19,22 @@ {{system.concept}} {{/if}}
+ + {{!-- Type PNJ (standard / antagoniste) --}} +
+ {{#if isEditMode}} + + {{else}} + {{#if (eq system.npcType "antagoniste")}} + {{localize "CELESTOPOL.NPC.typeAntagoniste"}} + {{/if}} + {{/if}} +
+
@@ -49,9 +65,20 @@
{{/if}} {{/with}} + + {{!-- Badge malus armure équipée --}} + {{#if armorMalus}} +
+ + {{armorMalus}} +
+ {{/if}}
+ + 🌙 + diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs index de6f42e..8a44666 100644 --- a/templates/roll-dialog.hbs +++ b/templates/roll-dialog.hbs @@ -27,38 +27,83 @@ {{#if woundMalus}} − {{abs woundMalus}} {{/if}} + {{#if armorMalus}} + − {{abs armorMalus}} + {{/if}}
{{#if woundLabel}}
⚠ {{woundLabel}}
{{/if}} + {{#if armorMalus}} +
🛡 {{localize "CELESTOPOL.Roll.armorMalus"}}
+ {{/if}}
- {{!-- Difficulté : sélect standard OU input Corps PNJ en combat --}} + {{!-- Difficulté : Corps PNJ en combat, fixe 11 en test normal --}} {{#if isCombat}} -
- - -
- {{else}} -
- - + + {{#each availableTargets as |t|}} + {{/each}}
{{/if}} +
+ + +
+ + + {{!-- Modificateurs tir (distance uniquement) --}} + {{#if isRangedAttack}} +
+ + +
+ {{/if}} + + {{else}} +
+ + 11 +
+ + {{!-- Test en opposition : le résultat sera masqué, MJ décide --}} + {{#unless isResistance}} +
+ +
+ {{/unless}} + + {{/if}} + {{!-- Options non disponibles en test de résistance --}} {{#unless isResistance}} - {{!-- Modificateur & Aspect côte à côte --}} + {{!-- Modificateur & Aspect côte à côte (tests normaux) --}}
@@ -78,6 +123,23 @@
+ {{else}} + + {{!-- En résistance : Bonus/Malus d'Aspect disponible --}} +
+ + +
+ + {{/unless}}{{!-- /isResistance aspect --}} + + {{!-- Options non disponibles en test de résistance (lune, destin, puiser, fortune) --}} + {{#unless isResistance}} + {{!-- Dé de la Lune --}}
{{/if}} - {{/unless}}{{!-- /isResistance --}} + {{/unless}}{{!-- /isResistance (lune, destin, puiser, fortune) --}} {{!-- Modificateur de situation (-8 à +8) — tous les jets --}}