IMplémentation de la ajorité des remarques de Nepherius

This commit is contained in:
2026-04-06 17:48:30 +02:00
parent a3f7b11f82
commit 1022597bf8
51 changed files with 1900 additions and 443 deletions

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation de l'Esprit</strong> (Instruction, Merveilleux technologique, Raisonnement, Traitement), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>", "technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation de l'Esprit</strong> (Instruction, Merveilleux technologique, Raisonnement, Traitement), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>",
"narratif": "<p>Le personnage entre <strong>en contact avec l'esprit d'un défunt</strong>. Il peut lui poser une <em>question fermée</em> (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.</p>", "narratif": "<p>Le protagoniste entre <strong>en contact avec l'esprit d'un défunt</strong>. Il peut lui poser une <em>question fermée</em> (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.</p>",
"exemples": "<ul><li>Interroger la victime d'un meurtre sur l'identité de son agresseur.</li><li>Consulter l'esprit d'un ancêtre pour retrouver un objet caché.</li><li>Demander à un fantôme si quelqu'un l'a aidé à mourir.</li></ul>" "exemples": "<ul><li>Interroger la victime d'un meurtre sur l'identité de son agresseur.</li><li>Consulter l'esprit d'un ancêtre pour retrouver un objet caché.</li><li>Demander à un fantôme si quelqu'un l'a aidé à mourir.</li></ul>"
}, },
"_key": "!items!anomCommMorts001" "_key": "!items!anomCommMorts001"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, le protagoniste gagne la possibilité de <strong>relancer le Dé de Lune</strong> un nombre de fois égal à son Niveau d'Anomalie. Il peut choisir de conserver le résultat préféré.</p><p>Cette capacité ne s'applique pas aux tests de chance.</p>", "technique": "<p>Durant un scénario, le protagoniste gagne la possibilité de <strong>relancer le Dé de Lune</strong> un nombre de fois égal à son Niveau d'Anomalie. Il peut choisir de conserver le résultat préféré.</p><p>Cette capacité ne s'applique pas aux tests de chance.</p>",
"narratif": "<p>Le personnage peut <strong>influencer le hasard</strong> à 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.</p>", "narratif": "<p>Le protagoniste peut <strong>influencer le hasard</strong> à 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.</p>",
"exemples": "<ul><li>Obtenir une bonne main au jeu de cartes lors de la distribution.</li><li>Voir le feu passer au vert en tournant le coin de la rue.</li><li>Faire tomber le verre d'un convive gênant au bon moment.</li></ul>" "exemples": "<ul><li>Obtenir une bonne main au jeu de cartes lors de la distribution.</li><li>Voir le feu passer au vert en tournant le coin de la rue.</li><li>Faire tomber le verre d'un convive gênant au bon moment.</li></ul>"
}, },
"_key": "!items!anomEntropie0001" "_key": "!items!anomEntropie0001"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test de <strong>Coercition, Échauffourée, Effacement ou Traque</strong>, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>", "technique": "<p>Durant un scénario, lors d'un test de <strong>Coercition, Échauffourée, Effacement ou Traque</strong>, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>",
"narratif": "<p>Le personnage peut <strong>générer une petite illusion mineure</strong> — visuelle, auditive ou olfactive, au choix — sans détail ni précision, pour une durée d'<strong>une minute</strong>. L'illusion ne peut représenter un être vivant en détail et ne résiste pas à un examen rapproché.</p>", "narratif": "<p>Le protagoniste peut <strong>générer une petite illusion mineure</strong> — visuelle, auditive ou olfactive, au choix — sans détail ni précision, pour une durée d'<strong>une minute</strong>. L'illusion ne peut représenter un être vivant en détail et ne résiste pas à un examen rapproché.</p>",
"exemples": "<ul><li>Le son d'un chat qui miaule ou d'un livre qui tombe dans la pièce voisine.</li><li>Un reflet métallique ou une ombre fugace au bout d'un couloir.</li><li>L'odeur de la pluie ou d'une fumée inquiétante.</li></ul>" "exemples": "<ul><li>Le son d'un chat qui miaule ou d'un livre qui tombe dans la pièce voisine.</li><li>Un reflet métallique ou une ombre fugace au bout d'un couloir.</li><li>L'odeur de la pluie ou d'une fumée inquiétante.</li></ul>"
}, },
"_key": "!items!anomIllusion0001" "_key": "!items!anomIllusion0001"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation de l'Âme</strong> (Artifice, Attraction, Coercition, Faveur), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>", "technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation de l'Âme</strong> (Artifice, Attraction, Coercition, Faveur), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>",
"narratif": "<p>Le personnage est capable d'<strong>influencer la prise de décision</strong> d'une personne en lui parlant à voix haute et en la regardant dans les yeux. Cette décision doit avoir un <em>impact immédiat</em> sur l'action de la personne concernée.</p><p>Cette capacité fonctionne également sur les <strong>automates sophistiqués de 4e et 5e génération</strong>.</p>", "narratif": "<p>Le protagoniste est capable d'<strong>influencer la prise de décision</strong> d'une personne en lui parlant à voix haute et en la regardant dans les yeux. Cette décision doit avoir un <em>impact immédiat</em> sur l'action de la personne concernée.</p><p>Cette capacité fonctionne également sur les <strong>automates sophistiqués de 4e et 5e génération</strong>.</p>",
"exemples": "<ul><li>Convaincre un garde de laisser passer sans vérifier les laissez-passer.</li><li>Pousser un prisonnier à donner son nom ou à s'asseoir.</li><li>Inciter un chauffeur de taxi à emprunter un itinéraire détourné.</li></ul>" "exemples": "<ul><li>Convaincre un garde de laisser passer sans vérifier les laissez-passer.</li><li>Pousser un prisonnier à donner son nom ou à s'asseoir.</li><li>Inciter un chauffeur de taxi à emprunter un itinéraire détourné.</li></ul>"
}, },
"_key": "!items!anomSuggestion01" "_key": "!items!anomSuggestion01"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation du Cœur</strong> (Appréciation, Arts, Inspiration, Traque), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>", "technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation du Cœur</strong> (Appréciation, Arts, Inspiration, Traque), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>",
"narratif": "<p>En <strong>tirant les cartes</strong>, le personnage peut apprendre une information sur une personne concernant son <em>passé, son présent ou son avenir</em>. L'information reste sujette à interprétation et le narrateur peut choisir de la formuler de façon symbolique ou métaphorique.</p>", "narratif": "<p>En <strong>tirant les cartes</strong>, le protagoniste peut apprendre une information sur une personne concernant son <em>passé, son présent ou son avenir</em>. L'information reste sujette à interprétation et le narrateur peut choisir de la formuler de façon symbolique ou métaphorique.</p>",
"exemples": "<ul><li>Deviner où se trouvera une cible le lendemain.</li><li>Connaître les antécédents douloureux d'un voisin mystérieux.</li><li>Obtenir une image symbolique du danger qui attend un allié.</li></ul>" "exemples": "<ul><li>Deviner où se trouvera une cible le lendemain.</li><li>Connaître les antécédents douloureux d'un voisin mystérieux.</li><li>Obtenir une image symbolique du danger qui attend un allié.</li></ul>"
}, },
"_key": "!items!anomTarot00001" "_key": "!items!anomTarot00001"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation du Corps</strong> (Échauffourée, Effacement, Mobilité, Prouesse), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>", "technique": "<p>Durant un scénario, lors d'un test d'une <strong>Spécialisation du Corps</strong> (Échauffourée, Effacement, Mobilité, Prouesse), le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>",
"narratif": "<p>Dans un rayon de <strong>8 mètres</strong>, le personnage peut <strong>déplacer par la pensée</strong> un petit objet léger sans attaches, sur <strong>4 mètres</strong> (dans n'importe quelle direction) pendant <strong>2 tours</strong>. L'objet doit être visible et accessible par le regard.</p>", "narratif": "<p>Dans un rayon de <strong>8 mètres</strong>, le protagoniste peut <strong>déplacer par la pensée</strong> un petit objet léger sans attaches, sur <strong>4 mètres</strong> (dans n'importe quelle direction) pendant <strong>2 tours</strong>. L'objet doit être visible et accessible par le regard.</p>",
"exemples": "<ul><li>Déplacer une cuillère pour la faire tomber d'une table au bon moment.</li><li>Faire léviter un jeu de tarot ou un trousseau de clés.</li><li>Pousser doucement un verre pour attirer l'attention d'un interlocuteur.</li></ul>" "exemples": "<ul><li>Déplacer une cuillère pour la faire tomber d'une table au bon moment.</li><li>Faire léviter un jeu de tarot ou un trousseau de clés.</li><li>Pousser doucement un verre pour attirer l'attention d'un interlocuteur.</li></ul>"
}, },
"_key": "!items!anomTelekines01" "_key": "!items!anomTelekines01"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test d'<strong>Appréciation, Attraction, Échauffourée ou Faveur</strong>, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p><p>Cette capacité fonctionne également sur les <strong>automates sophistiqués de 4e et 5e génération</strong>.</p>", "technique": "<p>Durant un scénario, lors d'un test d'<strong>Appréciation, Attraction, Échauffourée ou Faveur</strong>, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p><p>Cette capacité fonctionne également sur les <strong>automates sophistiqués de 4e et 5e génération</strong>.</p>",
"narratif": "<p>Le personnage est capable de <strong>percevoir les pensées superficielles</strong> 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 <em>l'observant</em>.</p>", "narratif": "<p>Le protagoniste est capable de <strong>percevoir les pensées superficielles</strong> 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 <em>l'observant</em>.</p>",
"exemples": "<ul><li>Percevoir l'image d'un cristal de cyanure dans l'esprit d'un serviteur soupçonné de tentative de meurtre.</li><li>Détecter, malgré un visage parfaitement contrôlé, qu'un magistrat est en réalité terrifié.</li><li>Ressentir la culpabilité d'un homme qui ment avec aplomb.</li></ul>" "exemples": "<ul><li>Percevoir l'image d'un cristal de cyanure dans l'esprit d'un serviteur soupçonné de tentative de meurtre.</li><li>Détecter, malgré un visage parfaitement contrôlé, qu'un magistrat est en réalité terrifié.</li><li>Ressentir la culpabilité d'un homme qui ment avec aplomb.</li></ul>"
}, },
"_key": "!items!anomTelepathi01" "_key": "!items!anomTelepathi01"

View File

@@ -8,7 +8,7 @@
"level": 2, "level": 2,
"usesRemaining": 2, "usesRemaining": 2,
"technique": "<p>Durant un scénario, lors d'un test d'<strong>Appréciation, Merveilleux technologique, Traitement ou Traque</strong>, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>", "technique": "<p>Durant un scénario, lors d'un test d'<strong>Appréciation, Merveilleux technologique, Traitement ou Traque</strong>, le protagoniste gagne la possibilité de relancer les 2d8 un nombre de fois égal à son Niveau d'Anomalie.</p><p>Il doit conserver le dernier résultat.</p>",
"narratif": "<p>L'<strong>esprit du personnage quitte son enveloppe corporelle</strong> et se déplace de <strong>8 mètres par tour</strong> pendant <strong>4 tours</strong>, dans n'importe quelle direction. L'esprit est <em>invisible</em> et peut traverser tous les obstacles matériels. Les sens du personnage restent les mêmes durant le voyage.</p><p>Le corps reste immobile et vulnérable durant le voyage.</p>", "narratif": "<p>L'<strong>esprit du protagoniste quitte son enveloppe corporelle</strong> et se déplace de <strong>8 mètres par tour</strong> pendant <strong>4 tours</strong>, dans n'importe quelle direction. L'esprit est <em>invisible</em> et peut traverser tous les obstacles matériels. Les sens du protagoniste restent les mêmes durant le voyage.</p><p>Le corps reste immobile et vulnérable durant le voyage.</p>",
"exemples": "<ul><li>Accéder aux toits d'une maison pour effectuer une reconnaissance sans risque physique.</li><li>Inspecter une pièce adjacente verrouillée avant d'y pénétrer.</li><li>Voir à quelle distance un éboulement bloque le passage dans un tunnel et s'il y a des survivants.</li></ul>" "exemples": "<ul><li>Accéder aux toits d'une maison pour effectuer une reconnaissance sans risque physique.</li><li>Inspecter une pièce adjacente verrouillée avant d'y pénétrer.</li><li>Voir à quelle distance un éboulement bloque le passage dans un tunnel et s'il y a des survivants.</li></ul>"
}, },
"_key": "!items!anomVoyAstral01" "_key": "!items!anomVoyAstral01"

View File

@@ -17,6 +17,7 @@ import {
CelestopolActor, CelestopolActor,
CelestopolItem, CelestopolItem,
CelestopolChatMessage, CelestopolChatMessage,
CelestopolCombat,
CelestopolRoll, CelestopolRoll,
} from "./module/documents/_module.mjs" } from "./module/documents/_module.mjs"
import { import {
@@ -47,8 +48,11 @@ Hooks.once("init", () => {
} }
} }
// Expose SYSTEM constants in game.system namespace // Expose SYSTEM constants + utilities globales
game.celestopol = { SYSTEM } game.celestopol = {
SYSTEM,
rollMoonStandalone: (actor = null) => CelestopolRoll.rollMoonStandalone(actor),
}
// ── DataModels ────────────────────────────────────────────────────────── // ── DataModels ──────────────────────────────────────────────────────────
CONFIG.Actor.dataModels.character = CelestopolCharacter CONFIG.Actor.dataModels.character = CelestopolCharacter
@@ -64,8 +68,13 @@ Hooks.once("init", () => {
CONFIG.Actor.documentClass = CelestopolActor CONFIG.Actor.documentClass = CelestopolActor
CONFIG.Item.documentClass = CelestopolItem CONFIG.Item.documentClass = CelestopolItem
CONFIG.ChatMessage.documentClass = CelestopolChatMessage CONFIG.ChatMessage.documentClass = CelestopolChatMessage
CONFIG.Combat.documentClass = CelestopolCombat
CONFIG.Dice.rolls.push(CelestopolRoll) 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 ─────────────────────────────────────────────── // ── Token display defaults ───────────────────────────────────────────────
CONFIG.Actor.trackableAttributes = { CONFIG.Actor.trackableAttributes = {
character: { character: {
@@ -131,8 +140,6 @@ Hooks.once("init", () => {
/* ─── Ready hook ─────────────────────────────────────────────────────────── */ /* ─── Ready hook ─────────────────────────────────────────────────────────── */
/* ─── Ready hook ─────────────────────────────────────────────────────────── */
Hooks.once("ready", () => { Hooks.once("ready", () => {
console.log(`${SYSTEM_ID} | System ready`) console.log(`${SYSTEM_ID} | System ready`)
@@ -279,6 +286,14 @@ function _registerHandlebarsHelpers() {
// Helper : add two numbers // Helper : add two numbers
Handlebars.registerHelper("add", (a, b) => a + b) 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) { Handlebars.registerHelper("let", function(value, options) {
return options.fn({ value }) return options.fn({ value })
}) })
@@ -318,6 +333,8 @@ function _preloadTemplates() {
`${base}/npc-main.hbs`, `${base}/npc-main.hbs`,
`${base}/npc-competences.hbs`, `${base}/npc-competences.hbs`,
`${base}/npc-blessures.hbs`, `${base}/npc-blessures.hbs`,
`${base}/npc-equipement.hbs`,
`${base}/npc-biographie.hbs`,
`${base}/anomaly.hbs`, `${base}/anomaly.hbs`,
`${base}/aspect.hbs`, `${base}/aspect.hbs`,
`${base}/equipment.hbs`, `${base}/equipment.hbs`,
@@ -325,6 +342,7 @@ function _preloadTemplates() {
`${base}/armure.hbs`, `${base}/armure.hbs`,
`${base}/roll-dialog.hbs`, `${base}/roll-dialog.hbs`,
`${base}/chat-message.hbs`, `${base}/chat-message.hbs`,
`${base}/moon-standalone.hbs`,
`${base}/partials/item-scores.hbs`, `${base}/partials/item-scores.hbs`,
]) ])
} }

View File

@@ -1,7 +1,7 @@
{ {
"TYPES": { "TYPES": {
"Actor": { "Actor": {
"character": "Personnage", "character": "Protagoniste",
"npc": "PNJ" "npc": "PNJ"
}, },
"Item": { "Item": {
@@ -18,12 +18,14 @@
"concept": "Concept / Profession", "concept": "Concept / Profession",
"initiative": "Initiative", "initiative": "Initiative",
"anomaly": "Anomalie", "anomaly": "Anomalie",
"description": "Biographie", "descriptionPhysique": "Description physique",
"descriptionPsychologique": "Description psychologique",
"notes": "Notes", "notes": "Notes",
"metier": "Métier", "metier": "Métier",
"origine": "Origine", "origine": "Origine",
"age": "Âge", "age": "Âge",
"faction": "Faction" "faction": "Faction",
"description": "Description"
}, },
"Stat": { "Stat": {
"res": "Résistance", "res": "Résistance",
@@ -58,7 +60,7 @@
"resetUses": "Réinitialiser les utilisations (nouveau scénario)", "resetUses": "Réinitialiser les utilisations (nouveau scénario)",
"noAnomaly": "Aucune anomalie", "noAnomaly": "Aucune anomalie",
"noUsesLeft": "Plus d'utilisations disponibles pour ce scénario", "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", "applicableSkills": "Domaines applicables",
"moonDie": "Dé de lune", "moonDie": "Dé de lune",
"none": "Aucune", "none": "Aucune",
@@ -79,16 +81,16 @@
}, },
"Faction": { "Faction": {
"label": "Faction", "label": "Faction",
"score": "Score", "relation": "Niveau de Relation",
"custom": "Faction personnalisée…", "custom": "Faction personnalisée…",
"pinkerton": "Pinkerton", "pinkerton": "Agence Pinkerton",
"police": "Police", "police": "Police secrète du duc",
"okhrana": "Okhrana", "okhrana": "Okhrana",
"lunanovatek": "Luna NovaTek", "lunanovatek": "Luna NovaTek",
"oto": "OTO", "oto": "Société théosophique OTO",
"syndicats": "Syndicats", "syndicats": "Syndicats clandestins",
"vorovskoymir": "Vorovskoymir", "vorovskoymir": "Vorovskoy Mir",
"cour": "Cour" "cour": "Cour des merveilles"
}, },
"Track": { "Track": {
"blessures": "Blessures", "blessures": "Blessures",
@@ -96,6 +98,8 @@
"spleen": "Spleen", "spleen": "Spleen",
"level": "Niveau", "level": "Niveau",
"currentMalus": "Malus actuel", "currentMalus": "Malus actuel",
"blessuresTooltip": "Niveaux de blessures :\n12 : Anodin / Négligeable → aucun malus (1 min)\n34 : Dérisoire / Superficiel → 1 (10 min)\n56 : 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" "destinTooltip": "Usages du Destin :\n• Réaliser un test avec 3d8\n• Gagner l'initiative lors d'un combat\n• Trouver l'ensemble des indices\n• Éviter une blessure\n• Sortir de l'inconscience\n• Obtenir un Triomphe"
}, },
"Wound": { "Wound": {
@@ -115,6 +119,7 @@
"status": "État : " "status": "État : "
}, },
"Combat": { "Combat": {
"initiative": "Initiative",
"attack": "Attaquer", "attack": "Attaquer",
"corpsPnj": "Corps du PNJ", "corpsPnj": "Corps du PNJ",
"tie": "ÉGALITÉ", "tie": "ÉGALITÉ",
@@ -128,20 +133,31 @@
"rangedDefenseTag": "Défense à distance", "rangedDefenseTag": "Défense à distance",
"rangedDefenseSuccess": "Attaque esquivée — pas de blessure", "rangedDefenseSuccess": "Attaque esquivée — pas de blessure",
"rangedDefenseFailure": "Touché par le PNJ — 1 blessure", "rangedDefenseFailure": "Touché par le PNJ — 1 blessure",
"rangedDefensePlayerWounded":"Blessure infligée par attaque à distance" "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": { "Tab": {
"main": "Principal", "main": "Principal",
"competences": "Domaines", "competences": "Domaines",
"blessures": "Blessures", "blessures": "Jauges",
"factions": "Factions", "factions": "Factions",
"equipement": "Équipement", "equipement": "Équipement",
"biography": "Biographie", "biography": "Biographie",
"description": "Description", "description": "Description",
"technique": "Technique" "technique": "Technique",
"biographie": "Biographie",
"aspects": "Aspects"
}, },
"Roll": { "Roll": {
"clickToRoll": "Cliquer pour lancer", "clickToRoll": "Cliquer pour lancer",
"resThresholdHint": "Cette case déclenche la Résistance de la spécialisation",
"difficulty": "Difficulté", "difficulty": "Difficulté",
"modifier": "Modificateur", "modifier": "Modificateur",
"nbDice": "Nombre de dés", "nbDice": "Nombre de dés",
@@ -169,10 +185,15 @@
"criticalFailureDesc": "Marge ≤ 5 — résultat désastreux !", "criticalFailureDesc": "Marge ≤ 5 — résultat désastreux !",
"woundLevel": "Niveau de blessures", "woundLevel": "Niveau de blessures",
"threshold": "Seuil", "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", "baseDice": "2d8 de base",
"formula": "Formule", "formula": "Formule",
"rollMoonDie": "Lancer le Dé de la Lune", "rollMoonDie": "Lancer le Dé de la Lune",
"moonDieResult": "Dé de la Lune", "moonDieResult": "Dé de la Lune",
"armorMalus": "Malus armure équipée",
"visibility": "Visibilité", "visibility": "Visibilité",
"visibilityPublic": "Public", "visibilityPublic": "Public",
"visibilityGM": "MJ uniquement", "visibilityGM": "MJ uniquement",
@@ -185,10 +206,11 @@
"puiser": "Puiser dans ses ressources", "puiser": "Puiser dans ses ressources",
"puiserDesc": "Ignore tous les malus — coche 1 case de Spleen", "puiserDesc": "Ignore tous les malus — coche 1 case de Spleen",
"usedPuiser": "Ressources puisées — malus ignorés, +1 Spleen", "usedPuiser": "Ressources puisées — malus ignorés, +1 Spleen",
"situationMod": "Mod. de situation", "situationMod": "Modificateurs Situationnels",
"resistanceTest": "Test de résistance", "resistanceTest": "Test de résistance",
"resistanceClickToRoll": "Lancer un 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": { "Modifier": {
"evident": "Évident — Réussite automatique", "evident": "Évident — Réussite automatique",
@@ -215,7 +237,14 @@
"contrecoup": "Contrecoup", "contrecoup": "Contrecoup",
"contrecoupFull": "Complication négative significative, perte de 2 Destin, ou 4 au prochain jet", "contrecoupFull": "Complication négative significative, perte de 2 Destin, ou 4 au prochain jet",
"catastrophe": "Catastrophe", "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": { "Difficulty": {
"unknown": "Aucun seuil", "unknown": "Aucun seuil",
@@ -251,7 +280,8 @@
"newArmure": "Nouvelle armure", "newArmure": "Nouvelle armure",
"noWeapons": "Aucune arme", "noWeapons": "Aucune arme",
"noArmures": "Aucune armure", "noArmures": "Aucune armure",
"noEquipments": "Aucun équipement" "noEquipments": "Aucun équipement",
"noAspects": "Aucun aspect"
}, },
"Equipment": { "Equipment": {
"autre": "Autre", "autre": "Autre",
@@ -263,6 +293,11 @@
"Sheet": { "Sheet": {
"editMode": "Mode édition", "editMode": "Mode édition",
"playMode": "Mode jeu", "playMode": "Mode jeu",
"character": "Fiche Protagoniste",
"npc": "Fiche PNJ",
"anomaly": "Fiche Anomalie",
"aspect": "Fiche Aspect",
"equipment": "Fiche Équipement",
"weapon": "Fiche Arme", "weapon": "Fiche Arme",
"armure": "Fiche Armure" "armure": "Fiche Armure"
}, },
@@ -301,7 +336,14 @@
"protection": "Protection", "protection": "Protection",
"protectionHint": "Réduit les blessures subies de ce montant", "protectionHint": "Réduit les blessures subies de ce montant",
"malus": "Malus", "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": { "Aspect": {
"valeur": "Valeur" "valeur": "Valeur"
@@ -329,6 +371,20 @@
"refAugmenterAspect": "Augmenter / Diminuer un Aspect", "refAugmenterAspect": "Augmenter / Diminuer un Aspect",
"refAcquerirAttribut": "Acquérir ou augmenter un Attribut", "refAcquerirAttribut": "Acquérir ou augmenter un Attribut",
"refCoutAttributTotal": "= total des points × 10" "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"
} }
} }
} }

View File

@@ -29,6 +29,7 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
trackBox: CelestopolActorSheet.#onTrackBox, trackBox: CelestopolActorSheet.#onTrackBox,
skillLevel: CelestopolActorSheet.#onSkillLevel, skillLevel: CelestopolActorSheet.#onSkillLevel,
factionLevel: CelestopolActorSheet.#onFactionLevel, 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. */ /** 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) { static #onFactionLevel(_event, target) {
if (!this.isEditable) return if (!this.isEditable) return
const factionId = target.dataset.faction 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 current = this.document.system.factions[factionId]?.value ?? 0
const newValue = (index <= current) ? index - 1 : index // Cliquer sur le dot actif (sauf neutre) remet à 0
this.document.update({ [`system.factions.${factionId}.value`]: Math.max(0, newValue) }) const finalValue = (newValue === current && newValue !== 0) ? 0 : newValue
this.document.update({ [`system.factions.${factionId}.value`]: finalValue })
} }
} }

View File

@@ -17,6 +17,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses, resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
depenseXp: CelestopolCharacterSheet.#onDepenseXp, depenseXp: CelestopolCharacterSheet.#onDepenseXp,
supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog, supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
rollMoonDie: CelestopolCharacterSheet.#onRollMoonDie,
}, },
} }
@@ -93,15 +94,46 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
case "factions": case "factions":
context.tab = context.tabs.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 break
case "biography": case "biography":
context.tab = context.tabs.biography context.tab = context.tabs.biography
context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0 context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( context.enrichedDescriptionPhysique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.description, { async: true }) 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( context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.notes, { async: true }) doc.system.notes, { relativeTo: this.document })
break break
case "equipement": case "equipement":
@@ -235,4 +267,10 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
"system.xp.log": log, "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)
}
} }

View File

@@ -5,8 +5,14 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
/** @override */ /** @override */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["npc"], classes: ["npc"],
position: { width: 760, height: 600 }, position: { width: 780, height: 640 },
window: { contentClasses: ["npc-content"] }, window: { contentClasses: ["npc-content"] },
actions: {
createAspect: CelestopolNPCSheet.#onCreateAspect,
createWeapon: CelestopolNPCSheet.#onCreateWeapon,
createArmure: CelestopolNPCSheet.#onCreateArmure,
rollMoonDie: CelestopolNPCSheet.#onRollMoonDie,
},
} }
/** @override */ /** @override */
@@ -15,6 +21,8 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
tabs: { template: "templates/generic/tab-navigation.hbs" }, tabs: { template: "templates/generic/tab-navigation.hbs" },
competences: { template: "systems/fvtt-celestopol/templates/npc-competences.hbs" }, competences: { template: "systems/fvtt-celestopol/templates/npc-competences.hbs" },
blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.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" } tabGroups = { sheet: "competences" }
@@ -23,6 +31,8 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
const tabs = { const tabs = {
competences: { id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" }, 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" }, 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)) { for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id v.active = this.tabGroups[v.group] === v.id
@@ -36,9 +46,26 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
const context = await super._prepareContext() const context = await super._prepareContext()
context.tabs = this.#getTabs() context.tabs = this.#getTabs()
context.stats = SYSTEM.STATS context.stats = SYSTEM.STATS
context.skills = SYSTEM.SKILLS
context.anomalyTypes = SYSTEM.ANOMALY_TYPES context.anomalyTypes = SYSTEM.ANOMALY_TYPES
context.woundLevels = SYSTEM.WOUND_LEVELS 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 return context
} }
@@ -48,11 +75,59 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
switch (partId) { switch (partId) {
case "competences": case "competences":
context.tab = context.tabs.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 break
case "blessures": case "blessures":
context.tab = context.tabs.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 break
} }
return context 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)
}
} }

View File

@@ -171,6 +171,35 @@ export const WEAPON_RANGE_TYPES = {
longue: { id: "longue", label: "CELESTOPOL.Weapon.rangeLongue" }, 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 = { export const SYSTEM = {
id: SYSTEM_ID, id: SYSTEM_ID,
ASCII, ASCII,
@@ -180,6 +209,8 @@ export const SYSTEM = {
ANOMALY_TYPES, ANOMALY_TYPES,
ANOMALY_DEFINITIONS, ANOMALY_DEFINITIONS,
FACTIONS, FACTIONS,
NPC_TYPES,
ANTAGONISTE_STATS,
WOUND_LEVELS, WOUND_LEVELS,
DIFFICULTY_CHOICES, DIFFICULTY_CHOICES,
CONTEXT_MODIFIER_CHOICES, CONTEXT_MODIFIER_CHOICES,
@@ -189,4 +220,5 @@ export const SYSTEM = {
WEAPON_DAMAGE_TYPES, WEAPON_DAMAGE_TYPES,
WEAPON_RANGE_TYPES, WEAPON_RANGE_TYPES,
WEAPON_COMBAT_TYPES, WEAPON_COMBAT_TYPES,
RANGED_MODIFIERS,
} }

View File

@@ -1,4 +1,5 @@
export { default as CelestopolActor } from "./actor.mjs" export { default as CelestopolActor } from "./actor.mjs"
export { default as CelestopolItem } from "./item.mjs" export { default as CelestopolItem } from "./item.mjs"
export { default as CelestopolChatMessage } from "./chat-message.mjs" export { default as CelestopolChatMessage } from "./chat-message.mjs"
export { default as CelestopolCombat } from "./combat.mjs"
export { CelestopolRoll } from "./roll.mjs" export { CelestopolRoll } from "./roll.mjs"

View File

@@ -1,6 +1,22 @@
export default class CelestopolActor extends Actor { export default class CelestopolActor extends Actor {
/** @override */ /** @override */
getRollData() { 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
} }
} }

View File

@@ -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" } },
})
}
}

View File

@@ -36,6 +36,7 @@ export class CelestopolRoll extends Roll {
*/ */
static async prompt(options = {}) { static async prompt(options = {}) {
const woundMalus = options.woundMalus ?? 0 const woundMalus = options.woundMalus ?? 0
const armorMalus = options.armorMalus ?? 0
const skillValue = options.skillValue ?? 0 const skillValue = options.skillValue ?? 0
const woundLevelId = options.woundLevel ?? 0 const woundLevelId = options.woundLevel ?? 0
const destGaugeFull = options.destGaugeFull ?? false const destGaugeFull = options.destGaugeFull ?? false
@@ -46,6 +47,8 @@ export class CelestopolRoll extends Roll {
const weaponType = options.weaponType ?? "melee" const weaponType = options.weaponType ?? "melee"
const weaponName = options.weaponName ?? null const weaponName = options.weaponName ?? null
const weaponDegats = options.weaponDegats ?? "0" const weaponDegats = options.weaponDegats ?? "0"
const availableTargets = options.availableTargets ?? []
const isRangedAttack = isCombat && !isRangedDefense && weaponType === "distance"
const woundLabel = woundLevelId > 0 const woundLabel = woundLevelId > 0
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "") ? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
: null : null
@@ -63,6 +66,11 @@ export class CelestopolRoll extends Roll {
const v = i - 8 const v = i - 8
return { value: v, label: v > 0 ? `+${v}` : `${v}` } 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 = { const dialogContext = {
actorName: options.actorName, actorName: options.actorName,
@@ -71,20 +79,22 @@ export class CelestopolRoll extends Roll {
skillValue, skillValue,
woundMalus, woundMalus,
woundLabel, woundLabel,
difficultyChoices: SYSTEM.DIFFICULTY_CHOICES,
defaultDifficulty: options.difficulty ?? "normal",
destGaugeFull,
defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
isCombat, isCombat,
isRangedDefense, isRangedDefense,
isRangedAttack,
weaponType, weaponType,
weaponName, weaponName,
weaponDegats, weaponDegats,
modifierChoices, modifierChoices,
aspectChoices, aspectChoices,
situationChoices, situationChoices,
rangedModChoices,
availableTargets,
fortuneValue, fortuneValue,
armorMalus,
destGaugeFull,
defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
} }
const content = await foundry.applications.handlebars.renderTemplate( const content = await foundry.applications.handlebars.renderTemplate(
@@ -104,8 +114,40 @@ export class CelestopolRoll extends Roll {
const wrap = dialog.element.querySelector('.roll-dialog-content') const wrap = dialog.element.querySelector('.roll-dialog-content')
if (!wrap) return if (!wrap) return
function hasMalus(mod, asp, sit) { // Sélection de cible PNJ : masque le champ Corps PNJ (valeur cachée)
return woundMalus < 0 || mod < 0 || asp < 0 || sit < 0 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() { function update() {
@@ -114,6 +156,7 @@ export class CelestopolRoll extends Roll {
const modifier = autoSucc ? 0 : (parseInt(rawMod ?? 0) || 0) const modifier = autoSucc ? 0 : (parseInt(rawMod ?? 0) || 0)
const aspectMod = parseInt(wrap.querySelector('#aspectModifier')?.value ?? 0) || 0 const aspectMod = parseInt(wrap.querySelector('#aspectModifier')?.value ?? 0) || 0
const situMod = parseInt(wrap.querySelector('#situationMod')?.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 useDestin = wrap.querySelector('#useDestin')?.checked
const useFort = wrap.querySelector('#useFortune')?.checked const useFort = wrap.querySelector('#useFortune')?.checked
const puiser = wrap.querySelector('#puiserRessources')?.checked const puiser = wrap.querySelector('#puiserRessources')?.checked
@@ -122,7 +165,7 @@ export class CelestopolRoll extends Roll {
// En résistance : pas de "Puiser" possible // En résistance : pas de "Puiser" possible
const puiserRow = wrap.querySelector('#puiser-row') const puiserRow = wrap.querySelector('#puiser-row')
if (puiserRow) { if (puiserRow) {
if (!isResistance && hasMalus(modifier, aspectMod, situMod)) { if (!isResistance && hasMalus(modifier, aspectMod, situMod, rangedMod)) {
puiserRow.style.display = '' puiserRow.style.display = ''
} else { } else {
puiserRow.style.display = 'none' puiserRow.style.display = 'none'
@@ -135,7 +178,9 @@ export class CelestopolRoll extends Roll {
const effMod = puiser ? Math.max(0, modifier) : modifier const effMod = puiser ? Math.max(0, modifier) : modifier
const effAspect = puiser ? Math.max(0, aspectMod) : aspectMod const effAspect = puiser ? Math.max(0, aspectMod) : aspectMod
const effSit = puiser ? Math.max(0, situMod) : situMod 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 let formula
if (autoSucc) { if (autoSucc) {
@@ -153,7 +198,7 @@ export class CelestopolRoll extends Roll {
if (previewEl) previewEl.textContent = formula 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 => { .forEach(el => {
el.addEventListener('change', update) el.addEventListener('change', update)
el.addEventListener('input', update) el.addEventListener('input', update)
@@ -179,16 +224,18 @@ export class CelestopolRoll extends Roll {
if (!rollContext) return null 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 corpsPnj = isCombat ? (parseInt(rollContext.corpsPnj ?? 7) || 7) : null
const difficulty = isCombat ? "combat" : (rollContext.difficulty ?? "normal") const difficulty = isCombat ? "combat" : "standard"
const diffConfig = isCombat const diffConfig = isCombat
? { value: corpsPnj, label: "CELESTOPOL.Combat.corpsPnj" } ? { 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 autoSuccess = rollContext.modifier === "auto"
const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0) const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0)
const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0 const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0
const situationMod = parseInt(rollContext.situationMod ?? 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 useDestin = destGaugeFull && (rollContext.useDestin === true || rollContext.useDestin === "true")
const useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true") const useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true")
const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "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 // Puiser dans ses ressources → ignorer tous les malus
const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus
const effectiveArmorMalus = effectivePuiser ? 0 : armorMalus
const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier
const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod
const effectiveSituationMod = effectivePuiser ? Math.max(0, situationMod) : situationMod const effectiveSituationMod = effectivePuiser ? Math.max(0, situationMod) : situationMod
const effectiveRangedMod = effectivePuiser ? Math.max(0, rangedMod) : rangedMod
// Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8 // Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8
const nbDice = (!isResistance && useDestin) ? 3 : 2 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) const formula = (!isResistance && useFortune)
? buildFormula(1, totalModifier + 8) ? buildFormula(1, totalModifier + 8)
: buildFormula(nbDice, totalModifier) : buildFormula(nbDice, totalModifier)
@@ -232,11 +281,13 @@ export class CelestopolRoll extends Roll {
woundMalus: effectiveWoundMalus, woundMalus: effectiveWoundMalus,
autoSuccess, autoSuccess,
isResistance, isResistance,
isOpposition,
isCombat, isCombat,
isRangedDefense, isRangedDefense,
weaponType, weaponType,
weaponName, weaponName,
weaponDegats, weaponDegats,
rangedMod: effectiveRangedMod,
useDestin: !isResistance && useDestin, useDestin: !isResistance && useDestin,
useFortune: !isResistance && useFortune, useFortune: !isResistance && useFortune,
puiserRessources: effectivePuiser, puiserRessources: effectivePuiser,
@@ -294,9 +345,11 @@ export class CelestopolRoll extends Roll {
} }
} }
// Mémoriser les préférences // 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.rollMoonDie"] = rollData.rollMoonDie
updateData["system.prefs.difficulty"] = difficulty updateData["system.prefs.difficulty"] = difficulty
}
await actor.update(updateData) await actor.update(updateData)
} }
@@ -306,11 +359,8 @@ export class CelestopolRoll extends Roll {
/** /**
* Détermine succès/échec selon la marge (total seuil). * Détermine succès/échec selon la marge (total seuil).
* - Marge ≥ 5 → succès critique * Seuil : 11 pour les tests normaux, Corps PNJ pour le combat.
* - Marge > 0 → succès * Pas de succès/échec critique — seul le Dé de la Lune produit des résultats exceptionnels.
* - Marge = 0 → succès (ou égalité en combat)
* - Marge ≤ 5 → échec critique
* - Marge < 0 → échec
*/ */
computeResult() { computeResult() {
if (this.options.autoSuccess) { if (this.options.autoSuccess) {
@@ -318,9 +368,15 @@ export class CelestopolRoll extends Roll {
this.options.margin = null this.options.margin = null
return 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 const threshold = this.options.isCombat
? (this.options.difficultyValue ?? 0) ? (this.options.difficultyValue ?? 0)
: (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0) : 11
if (threshold === 0) { if (threshold === 0) {
this.options.resultType = "unknown" this.options.resultType = "unknown"
this.options.margin = null this.options.margin = null
@@ -330,10 +386,11 @@ export class CelestopolRoll extends Roll {
this.options.margin = margin this.options.margin = margin
if (this.options.isCombat && margin === 0) { if (this.options.isCombat && margin === 0) {
this.options.resultType = "tie" this.options.resultType = "tie"
} else if (margin >= 5) this.options.resultType = "critical-success" } else if (margin >= 0) {
else if (margin >= 0) this.options.resultType = "success" this.options.resultType = "success"
else if (margin <= -5) this.options.resultType = "critical-failure" } else {
else this.options.resultType = "failure" this.options.resultType = "failure"
}
} }
/** @override */ /** @override */
@@ -350,7 +407,7 @@ export class CelestopolRoll extends Roll {
const diceSum = diceResults.reduce((a, b) => a + b, 0) const diceSum = diceResults.reduce((a, b) => a + b, 0)
const threshold = this.options.isCombat const threshold = this.options.isCombat
? (this.options.difficultyValue ?? 0) ? (this.options.difficultyValue ?? 0)
: (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0) : 11
const margin = this.options.margin const margin = this.options.margin
const woundMalus = this.options.woundMalus ?? 0 const woundMalus = this.options.woundMalus ?? 0
const skillValue = this.options.skillValue ?? 0 const skillValue = this.options.skillValue ?? 0
@@ -365,18 +422,21 @@ export class CelestopolRoll extends Roll {
const moonResultType = this.options.moonResultType ?? null const moonResultType = this.options.moonResultType ?? null
const resultClassMap = { const resultClassMap = {
"critical-success": "critical-success",
"success": "success", "success": "success",
"tie": "tie", "tie": "tie",
"failure": "failure", "failure": "failure",
"critical-failure": "critical-failure", "opposition": "opposition",
"unknown": "", "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 const difficultyLabel = this.options.isCombat
? `${game.i18n.localize("CELESTOPOL.Combat.corpsPnj")} : ${threshold}` ? `${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 { return {
cssClass: [SYSTEM.id, "dice-roll"].join(" "), cssClass: [SYSTEM.id, "dice-roll"].join(" "),
@@ -394,11 +454,10 @@ export class CelestopolRoll extends Roll {
isSuccess: this.isSuccess, isSuccess: this.isSuccess,
isFailure: this.isFailure, isFailure: this.isFailure,
isTie: this.isTie, isTie: this.isTie,
isCriticalSuccess: this.isCriticalSuccess, isOpposition,
isCriticalFailure: this.isCriticalFailure,
difficulty: this.options.difficulty, difficulty: this.options.difficulty,
difficultyLabel, difficultyLabel,
difficultyValue: threshold, difficultyValue: isOpposition ? null : threshold,
margin, margin,
marginAbs: margin !== null ? Math.abs(margin) : null, marginAbs: margin !== null ? Math.abs(margin) : null,
marginAbove: margin !== null && margin >= 0, marginAbove: margin !== null && margin >= 0,
@@ -419,6 +478,8 @@ export class CelestopolRoll extends Roll {
weaponType: this.options.weaponType ?? null, weaponType: this.options.weaponType ?? null,
isRangedDefense: this.options.isRangedDefense ?? false, isRangedDefense: this.options.isRangedDefense ?? false,
woundTaken: this.options.woundTaken ?? null, woundTaken: this.options.woundTaken ?? null,
situationMod: this.options.situationMod ?? 0,
rangedMod: this.options.rangedMod ?? 0,
// Dé de lune // Dé de lune
hasMoonDie: moonDieResult !== null, hasMoonDie: moonDieResult !== null,
moonDieResult, moonDieResult,
@@ -442,4 +503,44 @@ export class CelestopolRoll extends Roll {
: `<strong>${skillLocalized}</strong>` : `<strong>${skillLocalized}</strong>`
return super.toMessage({ flavor, ...messageData }, { rollMode }) 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,
})
}
} }

View File

@@ -74,9 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
vision: persoAttrField(), vision: persoAttrField(),
}) })
// Factions — score entier direct (0-9) // Factions — niveau de relation -4 (hostile) à +4 (allié), 0 = neutre
const factionField = () => new fields.SchemaField({ 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({ schema.factions = new fields.SchemaField({
pinkerton: factionField(), pinkerton: factionField(),
@@ -89,11 +89,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
cour: factionField(), cour: factionField(),
perso1: new fields.SchemaField({ perso1: new fields.SchemaField({
label: new fields.StringField({ required: true, nullable: false, initial: "" }), 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({ perso2: new fields.SchemaField({
label: new fields.StringField({ required: true, nullable: false, initial: "" }), 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,7 +114,8 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
}) })
// Description & notes // Description & notes
schema.description = 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 }) schema.notes = new fields.HTMLField({ required: true, textSearch: true })
// Données biographiques // Données biographiques
@@ -152,6 +153,20 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
// XP dépensée = somme des montants du log // XP dépensée = somme des montants du log
this.xp.depense = this.xp.log.reduce((sum, entry) => sum + entry.montant, 0) 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, skillLabel: skill.label,
skillValue: skill.value, skillValue: skill.value,
woundMalus: this.getWoundMalus(), woundMalus: this.getWoundMalus(),
armorMalus: this.getArmorMalus(),
woundLevel: this.blessures.lvl, woundLevel: this.blessures.lvl,
difficulty: this.prefs.difficulty, difficulty: this.prefs.difficulty,
rollMoonDie: this.prefs.rollMoonDie ?? false, rollMoonDie: this.prefs.rollMoonDie ?? false,
@@ -213,6 +229,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
skillLabel: "CELESTOPOL.Roll.resistanceTest", skillLabel: "CELESTOPOL.Roll.resistanceTest",
skillValue: statData.res, skillValue: statData.res,
woundMalus: this.getWoundMalus(), woundMalus: this.getWoundMalus(),
armorMalus: this.getArmorMalus(),
woundLevel: this.blessures.lvl, woundLevel: this.blessures.lvl,
isResistance: true, isResistance: true,
rollMoonDie: false, 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. * Lance une attaque avec une arme.
* Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur. * Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur.
@@ -247,6 +296,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label, skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
skillValue: echauffouree.value, skillValue: echauffouree.value,
woundMalus: this.getWoundMalus(), woundMalus: this.getWoundMalus(),
armorMalus: this.getArmorMalus(),
woundLevel: this.blessures.lvl, woundLevel: this.blessures.lvl,
rollMoonDie: this.prefs.rollMoonDie ?? false, rollMoonDie: this.prefs.rollMoonDie ?? false,
destGaugeFull: this.destin.lvl > 0, destGaugeFull: this.destin.lvl > 0,
@@ -256,6 +306,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
weaponType: item.system.type, weaponType: item.system.type,
weaponName: item.name, weaponName: item.name,
weaponDegats: item.system.degats, weaponDegats: item.system.degats,
availableTargets: this._getCombatTargets(),
}) })
} }
@@ -283,6 +334,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
skillLabel: SYSTEM.SKILLS.corps.mobilite.label, skillLabel: SYSTEM.SKILLS.corps.mobilite.label,
skillValue: mobilite.value, skillValue: mobilite.value,
woundMalus: this.getWoundMalus(), woundMalus: this.getWoundMalus(),
armorMalus: this.getArmorMalus(),
woundLevel: this.blessures.lvl, woundLevel: this.blessures.lvl,
rollMoonDie: this.prefs.rollMoonDie ?? false, rollMoonDie: this.prefs.rollMoonDie ?? false,
destGaugeFull: this.destin.lvl > 0, destGaugeFull: this.destin.lvl > 0,
@@ -292,6 +344,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
weaponType: "distance", weaponType: "distance",
weaponName: item.name, weaponName: item.name,
weaponDegats: "0", weaponDegats: "0",
availableTargets: this._getCombatTargets(),
}) })
} }
} }

View File

@@ -84,6 +84,7 @@ export class CelestopolArmure extends foundry.abstract.TypeDataModel {
return { return {
protection: new fields.NumberField({ ...reqInt, initial: 1, min: 1, max: 2 }), protection: new fields.NumberField({ ...reqInt, initial: 1, min: 1, max: 2 }),
malus: new fields.NumberField({ ...reqInt, initial: 0, min: 0, 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 }), description: new fields.HTMLField({ required: true, textSearch: true }),
} }
} }

View File

@@ -6,7 +6,10 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
const reqInt = { required: true, nullable: false, integer: true } const reqInt = { required: true, nullable: false, integer: true }
const schema = {} const schema = {}
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.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.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
schema.anomaly = new fields.SchemaField({ schema.anomaly = new fields.SchemaField({
@@ -15,42 +18,26 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
}) })
const skillField = (label) => new fields.SchemaField({ // PNJs : 4 domaines uniquement (pas de sous-compétences)
label: new fields.StringField({ required: true, initial: label }), const domainField = (statId) => new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
})
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 }), label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }),
res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), // res + wound malus actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
...skillSchema,
}) })
}
schema.stats = new fields.SchemaField({ schema.stats = new fields.SchemaField({
ame: statField("ame"), ame: domainField("ame"),
corps: statField("corps"), corps: domainField("corps"),
coeur: statField("coeur"), coeur: domainField("coeur"),
esprit: statField("esprit"), esprit: domainField("esprit"),
}) })
schema.blessures = new fields.SchemaField({ schema.blessures = new fields.SchemaField({
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }), lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
}) })
schema.prefs = new fields.SchemaField({ schema.histoire = new fields.HTMLField({ required: true, textSearch: true })
rollMoonDie: new fields.BooleanField({ required: true, initial: false }), schema.descriptionPhysique = new fields.HTMLField({ required: true, textSearch: true })
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.notes = new fields.HTMLField({ required: true, textSearch: true })
return schema return schema
@@ -61,11 +48,12 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
prepareDerivedData() { prepareDerivedData() {
super.prepareDerivedData() super.prepareDerivedData()
const malus = this.getWoundMalus() 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) this.initiative = Math.max(0, this.stats.corps.res + malus)
for (const stat of Object.values(this.stats)) { for (const stat of Object.values(this.stats)) {
stat.actuel = Math.max(0, stat.res + malus) stat.actuel = Math.max(0, stat.res + malus)
} }
this.armorMalus = this.getArmorMalus()
} }
getWoundMalus() { getWoundMalus() {
@@ -73,22 +61,43 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0 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 { CelestopolRoll } = await import("../documents/roll.mjs")
const skill = this.stats[statId][skillId] const statData = this.stats[statId]
if (!skill) return null if (!statData) return null
const isAntagoniste = this.npcType === "antagoniste"
const skillLabel = isAntagoniste
? SYSTEM.ANTAGONISTE_STATS[statId]?.label
: SYSTEM.STATS[statId]?.label
return CelestopolRoll.prompt({ return CelestopolRoll.prompt({
actorId: this.parent.id, actorId: this.parent.id,
actorName: this.parent.name, actorName: this.parent.name,
actorImage: this.parent.img, actorImage: this.parent.img,
statId, statId,
skillId, skillLabel,
skillLabel: skill.label, skillValue: statData.res,
skillValue: skill.value,
woundMalus: this.getWoundMalus(), woundMalus: this.getWoundMalus(),
difficulty: this.prefs.difficulty, armorMalus: this.getArmorMalus(),
rollMoonDie: this.prefs.rollMoonDie ?? false, woundLevel: this.blessures.lvl,
}) })
} }
/** Alias pour compatibilité avec le handler _onRoll (clic sans skillId). */
async rollResistance(statId) {
return this.roll(statId)
}
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1 @@
MANIFEST-000033 MANIFEST-000018

View File

@@ -1,15 +1,3 @@
2026/03/31-17:30:25.623001 7ff9fd9ff6c0 Recovering log #30 2026/04/06-17:46:52.532955 7f67ebfff6c0 Recovering log #15
2026/03/31-17:30:25.660522 7ff9fd9ff6c0 Delete type=3 #28 2026/04/06-17:46:52.543005 7f67ebfff6c0 Delete type=3 #13
2026/03/31-17:30:25.660594 7ff9fd9ff6c0 Delete type=0 #30 2026/04/06-17:46:52.543081 7f67ebfff6c0 Delete type=0 #15
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)

View File

@@ -1,15 +1,15 @@
2026/03/31-14:51:45.342017 7ff9fd1fe6c0 Recovering log #25 2026/04/05-21:02:44.634018 7f8249dff6c0 Recovering log #10
2026/03/31-14:51:45.425193 7ff9fd1fe6c0 Delete type=3 #23 2026/04/05-21:02:44.729398 7f8249dff6c0 Delete type=3 #8
2026/03/31-14:51:45.425262 7ff9fd1fe6c0 Delete type=0 #25 2026/04/05-21:02:44.729470 7f8249dff6c0 Delete type=0 #10
2026/03/31-16:02:12.525970 7ff7477ef6c0 Level-0 table #31: started 2026/04/06-00:09:38.933436 7f82177fe6c0 Level-0 table #16: started
2026/03/31-16:02:12.529284 7ff7477ef6c0 Level-0 table #31: 3529 bytes OK 2026/04/06-00:09:38.937122 7f82177fe6c0 Level-0 table #16: 3525 bytes OK
2026/03/31-16:02:12.536284 7ff7477ef6c0 Delete type=0 #29 2026/04/06-00:09:38.943462 7f82177fe6c0 Delete type=0 #14
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/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/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/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/03/31-16:02:12.547133 7ff7477ef6c0 Compacting 1@1 + 1@2 files 2026/04/06-00:09:38.966141 7f82177fe6c0 Compacting 1@1 + 1@2 files
2026/03/31-16:02:12.550304 7ff7477ef6c0 Generated table #32@1: 9 keys, 6638 bytes 2026/04/06-00:09:38.969869 7f82177fe6c0 Generated table #17@1: 9 keys, 6617 bytes
2026/03/31-16:02:12.550331 7ff7477ef6c0 Compacted 1@1 + 1@2 files => 6638 bytes 2026/04/06-00:09:38.969906 7f82177fe6c0 Compacted 1@1 + 1@2 files => 6617 bytes
2026/03/31-16:02:12.557451 7ff7477ef6c0 compacted to: files[ 0 0 1 0 0 0 0 ] 2026/04/06-00:09:38.976148 7f82177fe6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/03/31-16:02:12.557568 7ff7477ef6c0 Delete type=2 #27 2026/04/06-00:09:38.976266 7f82177fe6c0 Delete type=2 #12
2026/03/31-16:02:12.557744 7ff7477ef6c0 Delete type=2 #31 2026/04/06-00:09:38.976457 7f82177fe6c0 Delete type=2 #16
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/06-00:09:38.987710 7f82177fe6c0 Manual compaction at level-1 from '!items!null' @ 17 : 1 .. '!items!null' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@@ -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 × Domaines grid
.stats-grid { .stats-grid {
display: grid; display: grid;
@@ -100,6 +112,47 @@
text-align: center; 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 { .skills-list {
@@ -146,11 +199,36 @@
border-radius: 1px; border-radius: 1px;
background: rgba(255,255,255,0.3); background: rgba(255,255,255,0.3);
vertical-align: middle; vertical-align: middle;
position: relative;
transition: background 0.1s, border-color 0.1s; transition: background 0.1s, border-color 0.1s;
&.filled { &.filled {
background: var(--cel-orange); background: var(--cel-orange);
border-color: var(--cel-border); 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; } &[data-action] { cursor: pointer; }
} }
} }
@@ -201,12 +279,30 @@
font-family: var(--cel-font-title); font-family: var(--cel-font-title);
font-weight: bold; font-weight: bold;
text-transform: uppercase; 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; cursor: help;
border-bottom: 1px dashed currentColor; opacity: 0.7;
text-decoration: none; transition: opacity 0.15s;
flex-shrink: 0;
&:hover { opacity: 1; }
} }
} }
@@ -270,7 +366,7 @@
background-image: url("../assets/ui/fond_cadrille.jpg"); background-image: url("../assets/ui/fond_cadrille.jpg");
background-blend-mode: soft-light; background-blend-mode: soft-light;
color: var(--cel-orange); 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 { .faction-row {
@@ -299,8 +395,14 @@
border-radius: 1px; border-radius: 1px;
background: rgba(255,255,255,0.3); background: rgba(255,255,255,0.3);
transition: background 0.1s; transition: background 0.1s;
&.filled { background: var(--cel-orange); border-color: var(--cel-orange); }
&[data-action] { cursor: pointer; } &[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 { .faction-count {
@@ -345,6 +447,12 @@
.item-row { .item-row {
.cel-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 { .item-tag {
font-size: 0.75em; font-size: 0.75em;
padding: 1px 7px; padding: 1px 7px;
@@ -355,6 +463,12 @@
white-space: nowrap; white-space: nowrap;
&.malus { background: rgba(192,68,68,0.1); border-color: rgba(192,68,68,0.35); color: #922; } &.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 { .equip-empty {
@@ -489,34 +603,50 @@
margin-top: 6px; margin-top: 6px;
summary { summary {
font-size: 0.78em; font-size: 0.78em;
color: var(--cel-border); color: var(--cel-orange-light);
cursor: pointer; cursor: pointer;
letter-spacing: 0.03em; letter-spacing: 0.03em;
text-transform: uppercase; text-transform: uppercase;
user-select: none; user-select: none;
&:hover { color: var(--cel-green); } &:hover { color: var(--cel-orange); }
} }
.xp-ref-table { .xp-ref-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.78em; font-size: 0.82em;
margin-top: 5px; margin-top: 6px;
opacity: 0.85;
thead tr {
background: var(--cel-green);
background-image: url("../assets/ui/fond_cadrille.jpg");
background-blend-mode: soft-light;
color: var(--cel-orange);
}
th { th {
color: var(--cel-border); font-family: var(--cel-font-title);
font-size: 0.9em;
letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.85em; padding: 4px 8px;
letter-spacing: 0.03em;
padding: 2px 6px;
border-bottom: 1px solid rgba(196,154,26,0.25);
text-align: left; text-align: left;
border-bottom: 2px solid var(--cel-orange);
} }
td { td {
padding: 2px 6px; padding: 4px 8px;
border-bottom: 1px solid rgba(196,154,26,0.1); color: var(--cel-text, #2a1a00);
color: var(--cel-text-dark, #3a2a0a); 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;
} }
} }
} }

View File

@@ -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 ──────────────────────────────────────────────────────────────── // ─── Tabs ────────────────────────────────────────────────────────────────

View File

@@ -393,6 +393,7 @@
gap: 14px; gap: 14px;
justify-content: center; justify-content: center;
margin: 12px 0; margin: 12px 0;
flex-wrap: wrap;
} }
.armure-stat-box { .armure-stat-box {
display: flex; display: flex;
@@ -405,12 +406,37 @@
min-width: 110px; min-width: 110px;
label { font-size: 0.72em; text-transform: uppercase; color: var(--cel-orange-light); letter-spacing: 0.05em; } label { font-size: 0.72em; text-transform: uppercase; color: var(--cel-orange-light); letter-spacing: 0.05em; }
.armure-stat-value { .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); 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; 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; } .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);
}
}
}
}
} }
} }

View File

@@ -4,18 +4,49 @@
.fvtt-celestopol.npc { .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; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 12px; gap: 12px;
padding: 8px 0; padding: 8px 0;
.stat-block { .npc-domain-block {
border: 1px solid var(--cel-border); border: 1px solid var(--cel-border);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
}
.stat-header { .npc-domain-header {
background: var(--cel-green); background: var(--cel-green);
background-image: url("../assets/ui/fond_cadrille.jpg"); background-image: url("../assets/ui/fond_cadrille.jpg");
background-blend-mode: soft-light; background-blend-mode: soft-light;
@@ -23,56 +54,200 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 5px 8px; padding: 8px 12px;
border-bottom: 1px solid rgba(196,154,26,0.4); min-height: 60px;
.stat-name { .npc-domain-labels {
display: flex;
flex-direction: column;
gap: 2px;
.domain-label-primary {
font-family: var(--cel-font-title); font-family: var(--cel-font-title);
font-weight: bold; font-weight: bold;
font-size: 1.1em;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.9em; letter-spacing: 0.06em;
color: var(--cel-orange);
} }
.stat-res { .domain-label-secondary {
font-size: 0.75em;
color: rgba(220,170,80,0.7);
font-style: italic;
text-transform: uppercase;
letter-spacing: 0.04em;
}
}
.npc-domain-value-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px;
font-size: 0.8em;
label { color: var(--cel-orange-light); } // Mode édition : input nombre
.stat-res-value { font-weight: bold; color: var(--cel-orange); } input.domain-value-input {
.stat-actuel { width: 40px;
font-size: 0.9em; .cel-input-std();
color: rgba(255,200,0,0.7); 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;
gap: 5px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.15s, box-shadow 0.15s;
&: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); }
}
.domain-die-icon {
font-size: 1.2em;
color: rgba(220,170,80,0.7);
}
.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; font-style: italic;
} }
input[type="number"] { width: 30px; .cel-input-std(); } }
}
} }
} }
.skills-list { // ── Section Aspects ──────────────────────────────────────────────────────
background: var(--cel-cream); .npc-aspects-section {
margin-top: 12px;
border: 1px solid var(--cel-border);
border-radius: 4px;
overflow: hidden;
.skill-row { .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; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 8px;
padding: 3px 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); border-bottom: 1px solid rgba(122,92,32,0.18);
font-size: 0.85em; background: var(--cel-cream);
font-size: 0.9em;
gap: 8px;
&:nth-child(even) { background: var(--cel-cream-dark); } &:nth-child(even) { background: var(--cel-cream-dark); }
&.rollable { .cel-rollable(); } .item-name { flex: 1; }
.aspect-value {
.skill-name { flex: 1; } font-family: var(--cel-font-title);
.skill-value { font-weight: bold; color: var(--cel-orange); min-width: 24px; text-align: center; } font-weight: bold;
.skill-value-input { width: 36px; .cel-input-std(); text-align: center; } 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 { .track-section {
border: 1px solid var(--cel-border); border: 1px solid var(--cel-border);
border-radius: 4px; border-radius: 4px;

View File

@@ -857,4 +857,345 @@
padding: 2px 4px; 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;
}
}
} }

View File

@@ -10,6 +10,17 @@
<div class="anomaly-body"> <div class="anomaly-body">
<div class="armure-stats"> <div class="armure-stats">
<div class="armure-stat-box equipped-box">
<label>{{localize "CELESTOPOL.Armure.equipped"}}</label>
<div class="armure-stat-value">
<label class="equipped-switch">
<input type="checkbox" name="system.equipped" {{#if system.equipped}}checked{{/if}} {{#unless isEditable}}disabled{{/unless}}>
<span class="switch-label {{#if system.equipped}}on{{/if}}">
{{#if system.equipped}}{{localize "CELESTOPOL.Armure.equippedYes"}}{{else}}{{localize "CELESTOPOL.Armure.equippedNo"}}{{/if}}
</span>
</label>
</div>
</div>
<div class="armure-stat-box"> <div class="armure-stat-box">
<label>{{localize "CELESTOPOL.Armure.protection"}}</label> <label>{{localize "CELESTOPOL.Armure.protection"}}</label>
<div class="armure-stat-value"> <div class="armure-stat-value">

View File

@@ -1,9 +1,15 @@
<div class="tab biography {{tab.cssClass}}" data-group="sheet" data-tab="biography"> <div class="tab biography {{tab.cssClass}}" data-group="sheet" data-tab="biography">
{{!-- Description / Biographie --}} {{!-- Description Physique --}}
<div class="biography-section"> <div class="biography-section">
<div class="section-header">{{localize "CELESTOPOL.Actor.description"}}</div> <div class="section-header">{{localize "CELESTOPOL.Actor.descriptionPhysique"}}</div>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} {{formInput systemFields.descriptionPhysique enriched=enrichedDescriptionPhysique value=system.descriptionPhysique name="system.descriptionPhysique" toggled=true}}
</div>
{{!-- Description Psychologique --}}
<div class="biography-section">
<div class="section-header">{{localize "CELESTOPOL.Actor.descriptionPsychologique"}}</div>
{{formInput systemFields.descriptionPsychologique enriched=enrichedDescriptionPsychologique value=system.descriptionPsychologique name="system.descriptionPsychologique" toggled=true}}
</div> </div>
{{!-- Notes --}} {{!-- Notes --}}

View File

@@ -2,7 +2,10 @@
{{!-- Blessures --}} {{!-- Blessures --}}
<section class="track-section"> <section class="track-section">
<div class="track-header"> <div class="track-header">
<span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span> <span class="track-title">
{{localize "CELESTOPOL.Track.blessures"}}
<span class="track-help" title="{{localize 'CELESTOPOL.Track.blessuresTooltip'}}">?</span>
</span>
<span class="wound-malus">{{localize "CELESTOPOL.Track.currentMalus"}} : <span class="wound-malus">{{localize "CELESTOPOL.Track.currentMalus"}} :
<strong>{{lookup @root.woundLevels system.blessures.lvl 'malus'}}</strong> <strong>{{lookup @root.woundLevels system.blessures.lvl 'malus'}}</strong>
</span> </span>
@@ -24,8 +27,9 @@
{{!-- Destin --}} {{!-- Destin --}}
<section class="track-section"> <section class="track-section">
<div class="track-header"> <div class="track-header">
<span class="track-title track-title-destin" <span class="track-title">{{localize "CELESTOPOL.Track.destin"}}
title="{{localize 'CELESTOPOL.Track.destinTooltip'}}">{{localize "CELESTOPOL.Track.destin"}}</span> <span class="track-help" title="{{localize 'CELESTOPOL.Track.destinTooltip'}}">?</span>
</span>
</div> </div>
<div class="track-boxes destin-boxes"> <div class="track-boxes destin-boxes">
{{#each (range 8) as |lvl|}} {{#each (range 8) as |lvl|}}
@@ -42,7 +46,10 @@
{{!-- Spleen --}} {{!-- Spleen --}}
<section class="track-section"> <section class="track-section">
<div class="track-header"> <div class="track-header">
<span class="track-title">{{localize "CELESTOPOL.Track.spleen"}}</span> <span class="track-title">
{{localize "CELESTOPOL.Track.spleen"}}
<span class="track-help" title="{{localize 'CELESTOPOL.Track.spleenTooltip'}}">?</span>
</span>
</div> </div>
<div class="track-boxes spleen-boxes"> <div class="track-boxes spleen-boxes">
{{#each (range 8) as |lvl|}} {{#each (range 8) as |lvl|}}

View File

@@ -5,10 +5,11 @@
<div class="stat-block"> <div class="stat-block">
<div class="stat-header"> <div class="stat-header">
<span class="stat-name">{{localize stat.label}}</span> <span class="stat-name">{{localize stat.label}}</span>
<div class="stat-res {{#unless ../isEditMode}}rollable{{/unless}}" data-stat-id="{{statId}}" <div class="stat-res-btn {{#unless ../isEditMode}}rollable{{/unless}}" data-stat-id="{{statId}}"
title="{{localize 'CELESTOPOL.Roll.resistanceClickToRoll'}}"> title="{{localize 'CELESTOPOL.Roll.resistanceClickToRoll'}}">
<label>{{localize "CELESTOPOL.Stat.res"}}</label> <i class="fas fa-dice-d8 res-die-icon"></i>
<span class="stat-res-value">{{lookup (lookup ../system.stats statId) 'res'}}</span> <span class="res-label">{{localize "CELESTOPOL.Stat.res"}}</span>
<span class="res-value">{{lookup (lookup ../system.stats statId) 'res'}}</span>
</div> </div>
</div> </div>
<div class="skills-list"> <div class="skills-list">
@@ -19,8 +20,9 @@
<div class="skill-checkboxes-container"> <div class="skill-checkboxes-container">
<div class="skill-checkboxes"> <div class="skill-checkboxes">
{{#each (range 8) as |lvl|}} {{#each (range 8) as |lvl|}}
<span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}}" <span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}} {{#if (isResThreshold skillId lvl)}}res-threshold{{/if}}"
data-action="skillLevel" data-stat-id="{{statId}}" data-skill-id="{{skillId}}" data-index="{{lvl}}"></span> data-action="skillLevel" data-stat-id="{{statId}}" data-skill-id="{{skillId}}" data-index="{{lvl}}"
title="{{#if (isResThreshold skillId lvl)}}{{localize 'CELESTOPOL.Roll.resThresholdHint'}}{{/if}}"></span>
{{/each}} {{/each}}
</div> </div>
</div> </div>
@@ -33,7 +35,8 @@
<div class="skill-checkboxes-container"> <div class="skill-checkboxes-container">
<div class="skill-checkboxes"> <div class="skill-checkboxes">
{{#each (range 8) as |lvl|}} {{#each (range 8) as |lvl|}}
<span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}}"></span> <span class="skill-level-dot {{#if (lte lvl (lookup @root.system.stats statId skillId 'value'))}}filled{{/if}} {{#if (isResThreshold skillId lvl)}}res-threshold{{/if}}"
title="{{#if (isResThreshold skillId lvl)}}{{localize 'CELESTOPOL.Roll.resThresholdHint'}}{{/if}}"></span>
{{/each}} {{/each}}
</div> </div>
</div> </div>

View File

@@ -41,12 +41,17 @@
{{/if}} {{/if}}
</div> </div>
{{#each armures as |item|}} {{#each armures as |item|}}
<div class="item-row armure" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true"> <div class="item-row armure {{#if item.system.equipped}}is-equipped{{/if}}" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img src="{{item.img}}" class="item-icon"> <img src="{{item.img}}" class="item-icon">
<span class="item-name">{{item.name}}</span> <span class="item-name">{{item.name}}</span>
<span class="item-tag prot"><i class="fas fa-shield"></i> {{item.system.protection}}</span> <span class="item-tag prot"><i class="fas fa-shield"></i> {{item.system.protection}}</span>
{{#if item.system.malus}}<span class="item-tag malus">{{item.system.malus}} {{localize "CELESTOPOL.Armure.malus"}}</span>{{/if}} {{#if item.system.malus}}<span class="item-tag malus">{{item.system.malus}} {{localize "CELESTOPOL.Armure.malus"}}</span>{{/if}}
<div class="item-controls"> <div class="item-controls">
<a data-action="toggleArmure" data-item-uuid="{{item.uuid}}"
class="equip-toggle {{#if item.system.equipped}}equipped{{/if}}"
title="{{#if item.system.equipped}}{{localize 'CELESTOPOL.Armure.unequip'}}{{else}}{{localize 'CELESTOPOL.Armure.equip'}}{{/if}}">
<i class="fas fa-shield{{#unless item.system.equipped}}-halved{{/unless}}"></i>
</a>
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a> <a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}} {{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
</div> </div>

View File

@@ -3,72 +3,53 @@
<thead> <thead>
<tr> <tr>
<th>{{localize "CELESTOPOL.Faction.label"}}</th> <th>{{localize "CELESTOPOL.Faction.label"}}</th>
<th>{{localize "CELESTOPOL.Faction.score"}}</th> <th>{{localize "CELESTOPOL.Faction.relation"}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each factions as |faction factionId|}} {{!-- Factions standard --}}
<tr class="faction-row" data-faction="{{factionId}}"> {{#each factionRows as |faction|}}
<tr class="faction-row" data-faction="{{faction.id}}">
<td class="faction-name">{{localize faction.label}}</td> <td class="faction-name">{{localize faction.label}}</td>
<td class="faction-value"> <td class="faction-value">
<div class="faction-checkboxes-container"> <div class="faction-checkboxes-container">
<div class="faction-checkboxes"> <div class="faction-checkboxes">
{{#each (range 9) as |level|}} {{#each faction.dots as |dot|}}
<span class="faction-dot {{#if (lte level (lookup @root.system.factions factionId 'value'))}}filled{{/if}}" <span class="faction-dot {{dot.type}} {{#if dot.filled}}filled{{/if}}"
{{#if @root.isEditable}}data-action="factionLevel" data-faction="{{factionId}}" data-index="{{level}}"{{/if}}></span> {{#if @root.isEditable}}data-action="factionLevel" data-faction="{{../id}}" data-index="{{dot.index}}"{{/if}}></span>
{{/each}} {{/each}}
</div> </div>
<span class="faction-count">{{lookup @root.system.factions factionId 'value'}}</span> <span class="faction-count">{{faction.valueStr}}</span>
</div> </div>
</td> </td>
</tr> </tr>
{{/each}} {{/each}}
{{!-- Factions personnalisées --}} {{!-- Factions personnalisées --}}
<tr class="faction-row custom" data-faction="perso1"> {{#each factionCustom as |faction|}}
<tr class="faction-row custom" data-faction="{{faction.id}}">
<td> <td>
{{#if isEditMode}} {{#if @root.isEditMode}}
<input type="text" name="system.factions.perso1.label" <input type="text" name="system.factions.{{faction.id}}.label"
value="{{system.factions.perso1.label}}" value="{{faction.label}}"
placeholder="{{localize 'CELESTOPOL.Faction.custom'}}"> placeholder="{{localize 'CELESTOPOL.Faction.custom'}}">
{{else}} {{else}}
<span>{{#if system.factions.perso1.label}}{{system.factions.perso1.label}}{{else}}{{/if}}</span> <span>{{#if faction.label}}{{faction.label}}{{else}}{{/if}}</span>
{{/if}} {{/if}}
</td> </td>
<td> <td>
<div class="faction-checkboxes-container"> <div class="faction-checkboxes-container">
<div class="faction-checkboxes"> <div class="faction-checkboxes">
{{#each (range 9) as |level|}} {{#each faction.dots as |dot|}}
<span class="faction-dot {{#if (lte level ../system.factions.perso1.value)}}filled{{/if}}" <span class="faction-dot {{dot.type}} {{#if dot.filled}}filled{{/if}}"
{{#if ../isEditable}}data-action="factionLevel" data-faction="perso1" data-index="{{level}}"{{/if}}></span> {{#if @root.isEditable}}data-action="factionLevel" data-faction="{{../id}}" data-index="{{dot.index}}"{{/if}}></span>
{{/each}} {{/each}}
</div> </div>
<span class="faction-count">{{system.factions.perso1.value}}</span> <span class="faction-count">{{faction.valueStr}}</span>
</div> </div>
</td> </td>
</tr> </tr>
<tr class="faction-row custom" data-faction="perso2">
<td>
{{#if isEditMode}}
<input type="text" name="system.factions.perso2.label"
value="{{system.factions.perso2.label}}"
placeholder="{{localize 'CELESTOPOL.Faction.custom'}}">
{{else}}
<span>{{#if system.factions.perso2.label}}{{system.factions.perso2.label}}{{else}}{{/if}}</span>
{{/if}}
</td>
<td>
<div class="faction-checkboxes-container">
<div class="faction-checkboxes">
{{#each (range 9) as |level|}}
<span class="faction-dot {{#if (lte level ../system.factions.perso2.value)}}filled{{/if}}"
{{#if ../isEditable}}data-action="factionLevel" data-faction="perso2" data-index="{{level}}"{{/if}}></span>
{{/each}} {{/each}}
</div>
<span class="faction-count">{{system.factions.perso2.value}}</span>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -82,10 +82,19 @@
</div> </div>
{{/if}} {{/if}}
{{/with}} {{/with}}
{{#if system.armorMalus}}
<div class="header-stat armor-malus-badge">
<label>{{localize "CELESTOPOL.Armure.malusLabel"}}</label>
<span class="armor-malus-value">{{system.armorMalus}}</span>
</div>
{{/if}}
</div> </div>
</div> </div>
<div class="header-buttons"> <div class="header-buttons">
<a class="moon-standalone-btn" data-action="rollMoonDie" title="{{localize 'CELESTOPOL.Moon.standaloneTitle'}}">
🌙
</a>
<a class="toggle-sheet" data-action="toggleSheet" title="{{#if isEditMode}}{{localize 'CELESTOPOL.Sheet.playMode'}}{{else}}{{localize 'CELESTOPOL.Sheet.editMode'}}{{/if}}"> <a class="toggle-sheet" data-action="toggleSheet" title="{{#if isEditMode}}{{localize 'CELESTOPOL.Sheet.playMode'}}{{else}}{{localize 'CELESTOPOL.Sheet.editMode'}}{{/if}}">
<i class="fas {{#if isEditMode}}fa-eye{{else}}fa-edit{{/if}}"></i> <i class="fas {{#if isEditMode}}fa-eye{{else}}fa-edit{{/if}}"></i>
</a> </a>

View File

@@ -0,0 +1,27 @@
<div class="celestopol chat-initiative">
{{!-- En-tête : acteur --}}
<div class="roll-header">
{{#if actorImg}}
<img src="{{actorImg}}" class="actor-img" alt="{{actorName}}">
{{/if}}
<div class="roll-info">
<span class="actor-name">{{actorName}}</span>
<span class="skill-info">
<span class="skill-lbl">⚡ {{localize "CELESTOPOL.Combat.initiative"}}</span>
</span>
</div>
</div>
{{!-- Bandeau initiative --}}
<div class="initiative-banner">
<div class="initiative-score-wrap">
<span class="initiative-icon">⚡</span>
<span class="initiative-score">{{value}}</span>
</div>
{{#if detail}}
<span class="initiative-detail">{{detail}}</span>
{{/if}}
</div>
</div>

View File

@@ -71,7 +71,8 @@
<span class="fl-total">{{total}}</span> <span class="fl-total">{{total}}</span>
</div> </div>
{{!-- Seuil et marge --}} {{!-- Seuil et marge (masqué en opposition) --}}
{{#unless isOpposition}}
<div class="threshold-line"> <div class="threshold-line">
<span class="vs-wrap"> <span class="vs-wrap">
<span class="vs-label">vs</span> <span class="vs-label">vs</span>
@@ -84,6 +85,7 @@
</span> </span>
{{/if}} {{/if}}
</div> </div>
{{/unless}}
{{!-- Infos bonus (Destin, Fortune, Aspect) --}} {{!-- Infos bonus (Destin, Fortune, Aspect) --}}
{{#if useDestin}} {{#if useDestin}}
@@ -119,22 +121,14 @@
{{#if autoSuccess}} {{#if autoSuccess}}
<span class="result-icon">★</span> <span class="result-icon">★</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.autoSuccess"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.autoSuccess"}}</span>
{{else if isOpposition}}
<span class="result-icon">⚔</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.opposition"}}</span>
<span class="result-desc">{{localize "CELESTOPOL.Roll.oppositionResolved"}}</span>
{{else if isTie}} {{else if isTie}}
<span class="result-icon">⚖</span> <span class="result-icon">⚖</span>
<span class="result-label">{{localize "CELESTOPOL.Combat.tie"}}</span> <span class="result-label">{{localize "CELESTOPOL.Combat.tie"}}</span>
<span class="result-desc">{{localize "CELESTOPOL.Combat.tieDesc"}}</span> <span class="result-desc">{{localize "CELESTOPOL.Combat.tieDesc"}}</span>
{{else if isCriticalSuccess}}
<span class="result-icon">✦✦</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.criticalSuccess"}}</span>
{{#if isCombat}}
{{#if isRangedDefense}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseSuccess"}}</span>
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span>
{{/if}}
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Roll.criticalSuccessDesc"}}</span>
{{/if}}
{{else if isSuccess}} {{else if isSuccess}}
<span class="result-icon">✦</span> <span class="result-icon">✦</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.success"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.success"}}</span>
@@ -145,20 +139,6 @@
<span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span> <span class="result-desc">{{localize "CELESTOPOL.Combat.successHit"}}{{#if (gt weaponDegats "0")}} +{{weaponDegats}} {{localize "CELESTOPOL.Combat.weaponDamage"}}{{/if}}</span>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{else if isCriticalFailure}}
<span class="result-icon">✖✖</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.criticalFailure"}}</span>
{{#if isCombat}}
{{#if (eq weaponType "melee")}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.failureHit"}}</span>
{{else if isRangedDefense}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.rangedDefenseFailure"}}</span>
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Combat.distanceNoWound"}}</span>
{{/if}}
{{else}}
<span class="result-desc">{{localize "CELESTOPOL.Roll.criticalFailureDesc"}}</span>
{{/if}}
{{else if isFailure}} {{else if isFailure}}
<span class="result-icon">✖</span> <span class="result-icon">✖</span>
<span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span> <span class="result-label">{{localize "CELESTOPOL.Roll.failure"}}</span>

View File

@@ -0,0 +1,39 @@
<div class="celestopol-roll moon-standalone-card">
{{!-- En-tête --}}
<div class="moon-standalone-header">
<span class="moon-standalone-title">{{localize "CELESTOPOL.Moon.standaloneTitle"}}</span>
{{#if actorName}}
<span class="moon-standalone-actor">— {{actorName}}</span>
{{/if}}
</div>
{{!-- Résultat principal : phase + valeur --}}
<div class="moon-standalone-main {{moonResultClass}}">
<span class="moon-standalone-symbol">{{moonFaceSymbol}}</span>
<div class="moon-standalone-info">
<span class="moon-standalone-phase">{{moonFaceLabel}}</span>
<span class="moon-standalone-value">{{localize "CELESTOPOL.Moon.quantiteHint"}} : {{result}}</span>
</div>
</div>
{{!-- Interprétation Chance --}}
<div class="moon-interpret-row">
<span class="moon-interpret-label">{{localize "CELESTOPOL.Moon.chanceInterpret"}}</span>
{{#if isGoodFortune}}
<span class="moon-fortune bonne-fortune">{{localize "CELESTOPOL.Moon.bonneFortune"}}</span>
{{else}}
<span class="moon-fortune mauvaise-fortune">{{localize "CELESTOPOL.Moon.mauvaiseFortune"}}</span>
{{/if}}
</div>
{{!-- Interprétation Narrative --}}
<div class="moon-die-result {{moonResultClass}}">
<div class="moon-die-info">
<span class="moon-interpret-label">{{localize "CELESTOPOL.Moon.narrativeInterpret"}}</span>
<span class="moon-die-type">{{moonResultLabel}}</span>
<span class="moon-die-desc">{{moonResultDesc}}</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
<div class="tab biographie {{tab.cssClass}}" data-group="sheet" data-tab="biographie">
{{!-- Faction --}}
<div class="bio-section faction-section">
<div class="section-header">
<i class="fas fa-flag"></i>
<span>{{localize "CELESTOPOL.NPC.faction"}}</span>
</div>
{{#if isEditMode}}
<div class="form-row-line faction-select-row">
<label for="system.faction">{{localize "CELESTOPOL.NPC.factionLabel"}}</label>
<select name="system.faction" id="faction-select">
<option value="" {{#unless system.faction}}selected{{/unless}}>— {{localize "CELESTOPOL.NPC.factionNone"}} —</option>
{{#each factions as |faction key|}}
<option value="{{key}}" {{#if (eq key ../system.faction)}}selected{{/if}}>{{localize faction.label}}</option>
{{/each}}
</select>
</div>
{{else}}
<div class="faction-display">
{{#if system.faction}}
<span class="faction-name">{{localize (lookup (lookup factions system.faction) 'label')}}</span>
{{else}}
<span class="faction-none">{{localize "CELESTOPOL.NPC.factionNone"}}</span>
{{/if}}
</div>
{{/if}}
</div>
{{!-- Histoire --}}
<div class="bio-section">
<div class="section-header">
<i class="fas fa-scroll"></i>
<span>{{localize "CELESTOPOL.NPC.histoire"}}</span>
</div>
{{formInput systemFields.histoire enriched=enrichedHistoire value=system.histoire name="system.histoire" toggled=true}}
</div>
{{!-- Description Physique --}}
<div class="bio-section">
<div class="section-header">
<i class="fas fa-user"></i>
<span>{{localize "CELESTOPOL.NPC.descriptionPhysique"}}</span>
</div>
{{formInput systemFields.descriptionPhysique enriched=enrichedDescriptionPhysique value=system.descriptionPhysique name="system.descriptionPhysique" toggled=true}}
</div>
</div>

View File

@@ -1,7 +1,13 @@
<div class="tab blessures {{tab.cssClass}}" data-group="sheet" data-tab="blessures"> <div class="tab blessures {{tab.cssClass}}" data-group="sheet" data-tab="blessures">
<section class="track-section"> <section class="track-section">
<div class="track-header"> <div class="track-header">
<span class="track-title">{{localize "CELESTOPOL.Track.blessures"}}</span> <span class="track-title">
{{localize "CELESTOPOL.Track.blessures"}}
<span class="track-help" title="{{localize 'CELESTOPOL.Track.blessuresTooltip'}}">?</span>
</span>
<span class="wound-malus">{{localize "CELESTOPOL.Track.currentMalus"}} :
<strong>{{lookup @root.woundLevels system.blessures.lvl 'malus'}}</strong>
</span>
</div> </div>
<div class="track-boxes"> <div class="track-boxes">
{{#each (range 8) as |lvl|}} {{#each (range 8) as |lvl|}}
@@ -17,8 +23,9 @@
</div> </div>
</section> </section>
{{!-- Description --}} {{!-- Notes (libres) --}}
<div class="description-section"> <div class="description-section notes-section">
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} <div class="section-header"><i class="fas fa-note-sticky"></i> {{localize "CELESTOPOL.Actor.notes"}}</div>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</div> </div>
</div> </div>

View File

@@ -1,37 +1,65 @@
<div class="tab competences {{tab.cssClass}}" data-group="sheet" data-tab="competences"> <div class="tab competences {{tab.cssClass}}" data-group="sheet" data-tab="competences">
<div class="stats-grid">
{{!-- Grille des 4 domaines --}}
<div class="npc-domains-grid">
{{#each stats as |stat statId|}} {{#each stats as |stat statId|}}
<div class="stat-block"> <div class="npc-domain-block">
<div class="stat-header"> <div class="npc-domain-header">
<span class="stat-name">{{localize stat.label}}</span> {{!-- Double label : Âme / Emprise --}}
<div class="stat-res"> <div class="npc-domain-labels">
<label>{{localize "CELESTOPOL.Stat.res"}}</label> <span class="domain-label-primary">{{localize (lookup ../domainLabels statId)}}</span>
{{#if (eq ../system.npcType "antagoniste")}}
<span class="domain-label-secondary">{{localize (lookup ../stats statId 'label')}}</span>
{{else}}
<span class="domain-label-secondary">{{localize (lookup ../antagonisteStats statId 'label')}}</span>
{{/if}}
</div>
{{!-- Valeur du domaine --}}
<div class="npc-domain-value-wrap">
{{#if ../isEditMode}} {{#if ../isEditMode}}
<input type="number" name="system.stats.{{statId}}.res" <input type="number" name="system.stats.{{statId}}.res"
value="{{lookup ../system.stats statId 'res'}}" min="0" max="8"> value="{{lookup ../system.stats statId 'res'}}" min="0" max="8" class="domain-value-input">
{{else}} {{else}}
<span class="stat-res-value"> <div class="npc-domain-roll-btn rollable" data-stat-id="{{statId}}"
{{lookup ../system.stats statId 'actuel'}} / {{lookup ../system.stats statId 'res'}} title="{{localize 'CELESTOPOL.Roll.clickToRoll'}}">
<i class="fas fa-dice-d8 domain-die-icon"></i>
<span class="domain-value">{{lookup ../system.stats statId 'actuel'}}</span>
<span class="domain-value-base">/{{lookup ../system.stats statId 'res'}}</span>
</div>
{{/if}}
</div>
</div>
</div>
{{/each}}
</div>
{{!-- Aspects --}}
{{#if (or aspects.length isEditMode)}}
<div class="npc-aspects-section">
<div class="section-header">
<i class="fas fa-star"></i>
<span>{{localize "CELESTOPOL.Tab.aspects"}}</span>
{{#if isEditMode}}
<a data-action="createAspect" title="{{localize 'CELESTOPOL.Item.newAspect'}}"><i class="fas fa-plus"></i></a>
{{/if}}
</div>
{{#each aspects as |item|}}
<div class="item-row aspect-row" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<span class="item-name">{{item.name}}</span>
<span class="aspect-value {{#if (gt item.system.valeur 0)}}positive{{else if (lt item.system.valeur 0)}}negative{{/if}}">
{{#if (gt item.system.valeur 0)}}+{{/if}}{{item.system.valeur}}
</span> </span>
{{/if}} <div class="item-controls">
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
</div> </div>
</div> </div>
<div class="skills-list">
{{#each (lookup ../skills statId) as |skill skillId|}}
<div class="skill-row {{#unless ../isEditMode}}rollable{{/unless}}"
data-stat-id="{{statId}}" data-skill-id="{{skillId}}">
<span class="skill-name">{{localize skill.label}}</span>
{{#if ../isEditMode}}
<input type="number" name="system.stats.{{statId}}.{{skillId}}.value"
value="{{lookup (lookup ../system.stats statId) skillId 'value'}}"
min="0" max="8" class="skill-value-input">
{{else}} {{else}}
<span class="skill-value">{{lookup (lookup ../system.stats statId) skillId 'value'}}</span> {{#unless ../isEditMode}}
<p class="equip-empty">{{localize "CELESTOPOL.Item.noAspects"}}</p>
{{/unless}}
{{/each}}
</div>
{{/if}} {{/if}}
</div>
{{/each}}
</div>
</div>
{{/each}}
</div>
</div> </div>

View File

@@ -0,0 +1,58 @@
<div class="tab equipement {{tab.cssClass}}" data-group="sheet" data-tab="equipement">
{{!-- ── Armes ─────────────────────────────────────────────────────────── --}}
<div class="equip-section">
<div class="section-header">
<i class="fas fa-khanda"></i>
<span>{{localize "CELESTOPOL.Item.weapons"}}</span>
{{#if isEditMode}}
<a data-action="createWeapon" title="{{localize 'CELESTOPOL.Item.newWeapon'}}"><i class="fas fa-plus"></i></a>
{{/if}}
</div>
{{#each weapons as |item|}}
<div class="item-row weapon" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img src="{{item.img}}" class="item-icon">
<span class="item-name">{{item.name}}</span>
<span class="item-tag type">{{#if (eq item.system.type "melee")}}{{localize "CELESTOPOL.Weapon.typeMelee"}}{{else}}{{localize "CELESTOPOL.Weapon.typeDistance"}}{{/if}}</span>
<span class="item-tag dmg">{{localize "CELESTOPOL.Weapon.degats"}} {{item.system.degats}}</span>
<div class="item-controls">
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
</div>
</div>
{{else}}
<p class="equip-empty">{{localize "CELESTOPOL.Item.noWeapons"}}</p>
{{/each}}
</div>
{{!-- ── Armures ───────────────────────────────────────────────────────── --}}
<div class="equip-section">
<div class="section-header">
<i class="fas fa-shield-halved"></i>
<span>{{localize "CELESTOPOL.Item.armures"}}</span>
{{#if isEditMode}}
<a data-action="createArmure" title="{{localize 'CELESTOPOL.Item.newArmure'}}"><i class="fas fa-plus"></i></a>
{{/if}}
</div>
{{#each armures as |item|}}
<div class="item-row armure {{#if item.system.equipped}}is-equipped{{/if}}" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<img src="{{item.img}}" class="item-icon">
<span class="item-name">{{item.name}}</span>
<span class="item-tag prot"><i class="fas fa-shield"></i> {{item.system.protection}}</span>
{{#if item.system.malus}}<span class="item-tag malus">{{item.system.malus}} {{localize "CELESTOPOL.Armure.malus"}}</span>{{/if}}
<div class="item-controls">
<a data-action="toggleArmure" data-item-uuid="{{item.uuid}}"
title="{{#if item.system.equipped}}{{localize 'CELESTOPOL.Armure.unequip'}}{{else}}{{localize 'CELESTOPOL.Armure.equip'}}{{/if}}"
class="equip-toggle {{#if item.system.equipped}}equipped{{/if}}">
<i class="fas fa-shield{{#unless item.system.equipped}}-halved{{/unless}}"></i>
</a>
<a data-action="edit" data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
{{#if ../isEditMode}}<a data-action="delete" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>{{/if}}
</div>
</div>
{{else}}
<p class="equip-empty">{{localize "CELESTOPOL.Item.noArmures"}}</p>
{{/each}}
</div>
</div>

View File

@@ -19,6 +19,22 @@
<span class="concept-display">{{system.concept}}</span> <span class="concept-display">{{system.concept}}</span>
{{/if}} {{/if}}
</div> </div>
{{!-- Type PNJ (standard / antagoniste) --}}
<div class="npc-type-row">
{{#if isEditMode}}
<select name="system.npcType" class="npc-type-select">
{{#each npcTypes as |ntype key|}}
<option value="{{key}}" {{#if (eq key ../system.npcType)}}selected{{/if}}>{{localize ntype.label}}</option>
{{/each}}
</select>
{{else}}
{{#if (eq system.npcType "antagoniste")}}
<span class="npc-type-badge antagoniste">{{localize "CELESTOPOL.NPC.typeAntagoniste"}}</span>
{{/if}}
{{/if}}
</div>
<div class="header-stats-row"> <div class="header-stats-row">
<div class="header-stat"> <div class="header-stat">
<label>{{localize "CELESTOPOL.Actor.initiative"}}</label> <label>{{localize "CELESTOPOL.Actor.initiative"}}</label>
@@ -49,9 +65,20 @@
</div> </div>
{{/if}} {{/if}}
{{/with}} {{/with}}
{{!-- Badge malus armure équipée --}}
{{#if armorMalus}}
<div class="header-stat armor-malus-badge">
<label>{{localize "CELESTOPOL.Armure.malusLabel"}}</label>
<span>{{armorMalus}}</span>
</div>
{{/if}}
</div> </div>
</div> </div>
<div class="header-buttons"> <div class="header-buttons">
<a class="moon-standalone-btn" data-action="rollMoonDie" title="{{localize 'CELESTOPOL.Moon.standaloneTitle'}}">
🌙
</a>
<a data-action="toggleSheet"> <a data-action="toggleSheet">
<i class="fas {{#if isEditMode}}fa-eye{{else}}fa-edit{{/if}}"></i> <i class="fas {{#if isEditMode}}fa-eye{{else}}fa-edit{{/if}}"></i>
</a> </a>

View File

@@ -27,38 +27,83 @@
{{#if woundMalus}} {{#if woundMalus}}
<span class="dminus"> {{abs woundMalus}}</span> <span class="dminus"> {{abs woundMalus}}</span>
{{/if}} {{/if}}
{{#if armorMalus}}
<span class="dminus armor-malus"> {{abs armorMalus}}</span>
{{/if}}
</div> </div>
{{#if woundLabel}} {{#if woundLabel}}
<div class="wound-info">⚠ {{woundLabel}}</div> <div class="wound-info">⚠ {{woundLabel}}</div>
{{/if}} {{/if}}
{{#if armorMalus}}
<div class="wound-info armor-info">🛡 {{localize "CELESTOPOL.Roll.armorMalus"}}</div>
{{/if}}
</div> </div>
</div> </div>
<div class="roll-form-rows"> <div class="roll-form-rows">
{{!-- Difficulté : sélect standard OU input Corps PNJ en combat --}} {{!-- Difficulté : Corps PNJ en combat, fixe 11 en test normal --}}
{{#if isCombat}} {{#if isCombat}}
<div class="form-row-line form-corps-pnj">
<label for="corpsPnj">{{localize "CELESTOPOL.Combat.corpsPnj"}}</label> {{!-- Sélecteur de cible PNJ (si des tokens NPCs sont disponibles) --}}
<input type="number" id="corpsPnj" name="corpsPnj" value="7" min="1" max="30" class="corps-pnj-input"> {{#if availableTargets.length}}
</div> <div class="form-row-line form-target-row">
{{else}} <label for="targetSelect"><i class="fas fa-crosshairs"></i> {{localize "CELESTOPOL.Combat.targetLabel"}}</label>
<div class="form-row-line"> <select id="targetSelect" name="targetSelect">
<label for="difficulty">{{localize "CELESTOPOL.Roll.difficulty"}}</label> <option value="">— {{localize "CELESTOPOL.Combat.targetAuto"}} —</option>
<select id="difficulty" name="difficulty"> {{#each availableTargets as |t|}}
{{#each difficultyChoices as |diff key|}} <option value="{{t.corps}}">{{t.name}}</option>
<option value="{{key}}" {{#if (eq key ../defaultDifficulty)}}selected{{/if}}>
{{localize diff.label}}{{#if diff.value}} ({{diff.value}}){{/if}}
</option>
{{/each}} {{/each}}
</select> </select>
</div> </div>
{{/if}} {{/if}}
<div class="form-row-line form-corps-pnj" id="corps-pnj-row">
<label for="corpsPnj">{{localize "CELESTOPOL.Combat.corpsPnj"}}</label>
<input type="number" id="corpsPnj" name="corpsPnj" value="7" min="1" max="30" class="corps-pnj-input">
</div>
<div class="form-row-line form-target-confirmed" id="target-confirmed-row" style="display:none">
<span class="target-confirmed-badge"><i class="fas fa-crosshairs"></i> <span id="target-confirmed-name"></span></span>
</div>
{{!-- Modificateurs tir (distance uniquement) --}}
{{#if isRangedAttack}}
<div class="form-row-line form-ranged-mod">
<label for="rangedMod"><i class="fas fa-bullseye"></i> {{localize "CELESTOPOL.Combat.rangedMod"}}</label>
<select id="rangedMod" name="rangedMod">
{{#each rangedModChoices as |choice|}}
<option value="{{choice.value}}" {{#if (eq choice.id "none")}}selected{{/if}}>{{choice.label}}</option>
{{/each}}
</select>
</div>
{{/if}}
{{else}}
<div class="form-row-line form-threshold-fixed">
<label>{{localize "CELESTOPOL.Roll.threshold"}}</label>
<span class="threshold-value" id="threshold-display">11</span>
</div>
{{!-- Test en opposition : le résultat sera masqué, MJ décide --}}
{{#unless isResistance}}
<div class="form-opposition-row">
<label class="opposition-toggle" for="isOpposition">
<input type="checkbox" id="isOpposition" name="isOpposition">
<span class="opposition-icon">⚔</span>
<span class="opposition-text">
<span class="opposition-main">{{localize "CELESTOPOL.Roll.opposition"}}</span>
<span class="opposition-sub">{{localize "CELESTOPOL.Roll.oppositionDesc"}}</span>
</span>
</label>
</div>
{{/unless}}
{{/if}}
{{!-- Options non disponibles en test de résistance --}} {{!-- Options non disponibles en test de résistance --}}
{{#unless isResistance}} {{#unless isResistance}}
{{!-- Modificateur & Aspect côte à côte --}} {{!-- Modificateur & Aspect côte à côte (tests normaux) --}}
<div class="form-two-col"> <div class="form-two-col">
<div class="form-row-line"> <div class="form-row-line">
<label for="modifier">{{localize "CELESTOPOL.Roll.modifier"}}</label> <label for="modifier">{{localize "CELESTOPOL.Roll.modifier"}}</label>
@@ -78,6 +123,23 @@
</div> </div>
</div> </div>
{{else}}
{{!-- En résistance : Bonus/Malus d'Aspect disponible --}}
<div class="form-row-line form-resistance-aspect">
<label for="aspectModifier">{{localize "CELESTOPOL.Roll.aspect"}}</label>
<select id="aspectModifier" name="aspectModifier">
{{#each aspectChoices as |choice|}}
<option value="{{choice.value}}" {{#if (eq choice.value 0)}}selected{{/if}}>{{choice.label}}</option>
{{/each}}
</select>
</div>
{{/unless}}{{!-- /isResistance aspect --}}
{{!-- Options non disponibles en test de résistance (lune, destin, puiser, fortune) --}}
{{#unless isResistance}}
{{!-- Dé de la Lune --}} {{!-- Dé de la Lune --}}
<div class="form-moon-row"> <div class="form-moon-row">
<label class="moon-toggle" for="rollMoonDie"> <label class="moon-toggle" for="rollMoonDie">
@@ -132,7 +194,7 @@
</div> </div>
{{/if}} {{/if}}
{{/unless}}{{!-- /isResistance --}} {{/unless}}{{!-- /isResistance (lune, destin, puiser, fortune) --}}
{{!-- Modificateur de situation (-8 à +8) — tous les jets --}} {{!-- Modificateur de situation (-8 à +8) — tous les jets --}}
<div class="form-row-line form-situation-mod"> <div class="form-row-line form-situation-mod">