diff --git a/lang/en.json b/lang/en.json index 9f53c1d..4e183d0 100644 --- a/lang/en.json +++ b/lang/en.json @@ -241,6 +241,28 @@ "success_count": "Success Count", "success_required": "Success Required", "test_of": "Test of", + "roll_success": "Success", + "roll_failure": "Failure", + "roll_context": "Roll context", + "roll_pool": "Dice pool", + "roll_dice": "Dice detail", + "pool_ability": "Ability", + "pool_skill": "Skill", + "pool_specialty": "Specialty", + "pool_help": "Help", + "pool_tooling": "Equipment", + "pool_group": "Group", + "pool_selfControl": "Self-Control", + "pool_total": "Total", + "totem_used": "Totem", + "totem_keep": "kept", + "domain_bonus": "Domain bonus", + "domain_penalty": "Domain penalty", + "used": "used", + "die_regular": "dice", + "die_human": "human", + "die_adapted": "adapted", + "die_successes": "successes", "total": "Total", "totem_dice": "Totem Dice", "totem_hint": "Totem Hint", @@ -574,7 +596,7 @@ "name": "Repulsion" }, "tracks": { - "name": "Tracks" + "name": "Tracking" }, "crafting": { "name": "Crafting" @@ -604,7 +626,7 @@ "name": "Melee" }, "archery": { - "name": "Ranged" + "name": "Ranged Weapons" }, "throwing": { "name": "Throwing" @@ -656,6 +678,7 @@ "Round": "Round" }, "SIZE_LEVELS": { + "tiny": "Insect", "small": "Small", "medium": "Medium", "large": "Large" diff --git a/lang/fr.json b/lang/fr.json index 0f05774..41b2260 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -93,7 +93,7 @@ "AGE_TYPES": { "young": "Jeune", "adult": "Adulte", - "old": "Vieux" + "old": "Ancien" }, "IDENTITY": { "biography": "Biographie", @@ -188,6 +188,28 @@ "grant_reroll": "accorder des relances", "success_count": "nombre de succès", "success_required": "succès requis", + "roll_success": "Réussite", + "roll_failure": "Échec", + "roll_context": "Contexte du jet", + "roll_pool": "Composition du jet", + "roll_dice": "Détail des dés", + "pool_ability": "Caractéristique", + "pool_skill": "Compétence", + "pool_specialty": "Spécialité", + "pool_help": "Entraide", + "pool_tooling": "Équipement", + "pool_group": "Groupe", + "pool_selfControl": "Sang-Froid", + "pool_total": "Total", + "totem_used": "Totem", + "totem_keep": "gardé", + "domain_bonus": "Bonus de domaine", + "domain_penalty": "Malus de domaine", + "used": "utilisé", + "die_regular": "dés", + "die_human": "humains", + "die_adapted": "adaptés", + "die_successes": "succès", "error_no_actor_selected": "Vous n'avez pas de personnage attitré ou de token sélectionné", "error_cannot_reroll": "Vous ne pouvez pas relancer un dés sur ce jet", "error_no_rerolls_left": "Plus de relances possibles", @@ -433,6 +455,7 @@ "major": "Majeur" }, "SIZE_LEVELS": { + "tiny": "Insecte", "small": "Petit", "medium": "Moyen", "large": "Grand" @@ -596,7 +619,7 @@ "name": "Répulsion" }, "tracks": { - "name": "Pistage" + "name": "Traces" }, "crafting": { "name": "Artisanat" @@ -617,7 +640,7 @@ "name": "Armes à feu" }, "archery": { - "name": "Tir à l'arc" + "name": "Armes de Tir" }, "armory": { "name": "Armurerie" diff --git a/less/base.less b/less/base.less index ebb09e4..7bc97a5 100644 --- a/less/base.less +++ b/less/base.less @@ -44,6 +44,24 @@ body.system-vermine2047.theme-dark { color: #000000; } +// Core sheets the system doesn't restyle (RollTable, Journal, …) inherit the +// gray text overrides above, which are unreadable on their dark backgrounds. +// Restore Foundry's default light-on-dark text colors for those apps (dark theme only). +body.system-vermine2047.theme-dark { + .roll-table-sheet, + .journal-sheet, + .document-sheet, + .settings-sheet { + --color-text-primary: var(--color-light-2); + --color-text-emphatic: var(--color-light-1); + --color-text-secondary: var(--color-light-3); + --color-text-subtle: var(--color-light-5); + --color-text-light-highlight: #f0f0e0; + --color-text-light-heading: #c9c7b8; + --color-text-light-primary: #b5b3a4; + } +} + body.system-vermine2047.theme-dark .form-group > label { color: #777777; } @@ -97,6 +115,21 @@ ul.unstyled { } } +// Les fenêtres de choix (totem, acteur) peuvent dépasser la hauteur fixe : +// on autorise le défilement pour accéder à la totalité du contenu. +.application#TOTEM_PICKER .window-content, +.application#ACTOR_PICKER .window-content, +.application#TRAIT_SELECTOR .window-content { + overflow-y: auto; +} + +// La fiche de groupe contient beaucoup de zones (membres, objectifs, totem…) : +// dès que le contenu dépasse la hauteur de la fenêtre, on autorise le défilement +// pour ne pas perdre l'accès aux zones du bas. +.application.sheet.vermine2047.group .window-content { + overflow-y: auto; +} + .window-content .row { &.smb { margin-bottom: 0.25rem; } &.mdb { margin-bottom: 0.5rem; } @@ -113,6 +146,29 @@ ul.unstyled { .rollable { .transition(); + + // Affordance légère au repos : la zone est cliquable. + i.fa-dice-d10, + i.fa-dice { + font-size: 0.8em; + color: @color-acid-green; + } +} + +// Sur la fiche créature, les valeurs lançables (Attaque…) sont fortement +// matérialisées : pastille de dé + bordure pointillée. +.application.sheet.vermine2047.creature { + .rollable { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 1px 5px; + border-radius: 3px; + color: @theme-color-light; + border: 1px dashed fade(@color-acid-green, 45%); + background: fade(@color-acid-green, 8%); + cursor: pointer; + } } img.profile-img { @@ -420,7 +476,7 @@ body.system-vermine2047 img#logo { } .item-name { - padding-left: 5px; + padding: 0 2px 0 5px; text-align: left; } } @@ -674,6 +730,107 @@ ol.chat-log { color: @color-amber; } } + + // ── Verdict ─────────────────────────────────────────────────────── + .roll-verdict { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin: 6px 0; + padding: 6px 12px; + border-radius: 3px; + font-family: "DistressBlack"; + text-transform: uppercase; + font-size: @font-size-16; + letter-spacing: 0.5px; + + &.success { + color: @color-acid-green; + background: fade(@color-acid-green, 15%); + border: 1px solid @color-acid-green; + } + + &.failure { + color: @color-hemolymph; + background: fade(@color-hemolymph, 15%); + border: 1px solid @color-hemolymph; + } + } + + // ── Contexte du jet ─────────────────────────────────────────────── + div.roll-context { + gap: 6px 12px; + margin: 4px 0 6px; + padding: 6px 8px; + background: rgba(0, 0, 0, 0.06); + + .context-item { + display: inline-flex; + align-items: center; + gap: 5px; + + .context-label { + font-family: "DistressBlack"; + text-transform: uppercase; + font-size: @font-size-11; + opacity: 0.8; + } + + .context-value { + font-family: "Roboto", sans-serif; + font-size: @font-size-12; + font-weight: 600; + } + + &.totem-human .context-value { color: @totem-human-color; } + &.totem-adapted .context-value { color: @totem-adapted-color; } + } + } + + // ── Composition du jet ──────────────────────────────────────────── + div.roll-pool { + margin: 4px 0; + + h4 { + font-family: "DistressBlack"; + text-transform: uppercase; + font-size: @font-size-11; + margin: 4px 0; + border-bottom: none; + } + + .pool-breakdown { + gap: 4px 10px; + + .pool-item { + font-family: "Roboto", sans-serif; + font-size: @font-size-11; + + &.pool-total { + font-weight: 700; + color: @theme-color-light; + } + } + } + } + + // ── Résumé des dés ──────────────────────────────────────────────── + div.roll-dice-summary { + gap: 6px 12px; + justify-content: center; + margin: 4px 0 2px; + padding: 4px 8px; + background: rgba(0, 0, 0, 0.06); + + .die-summary { + font-family: "Roboto", sans-serif; + font-size: @font-size-11; + + &.human { color: @totem-human-color; } + &.adapted { color: @totem-adapted-color; } + } + } } div.item-card header img { diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index 03d6239..86b38d6 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -51,6 +51,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet { case "main": break case "abilities": context.tab = context.tabs.abilities + context.specialties = doc.itemTypes.specialty break case "totem": context.tab = context.tabs.totem diff --git a/module/applications/sheets/creature-sheet.mjs b/module/applications/sheets/creature-sheet.mjs index e5978c7..0719e9a 100644 --- a/module/applications/sheets/creature-sheet.mjs +++ b/module/applications/sheets/creature-sheet.mjs @@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs" export default class VermineCreatureSheetV2 extends VermineBaseActorSheet { + // La fiche créature s'ouvre en mode édition : Gabarit, Taille, Rôle et Meute + // se règlent dès l'ouverture (le mode jeu reste accessible via le toggle). + _sheetMode = this.constructor.SHEET_MODES.EDIT + static DEFAULT_OPTIONS = { classes: ["creature"], position: { width: 700, height: 650 }, @@ -61,10 +65,22 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet { break case "stats": context.tab = context.tabs.stats - context.patternLabel = doc.system.pattern?.value ? CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label : "" - context.sizeLabel = doc.system.size?.value || "" - context.roleLabel = doc.system.role?.value ? CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label : "" + context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : "" + context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : "" + context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : "" context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none") + // Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N) + const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {} + const sizeLevel = doc.system.size?.value ?? 0 + context.sizeCumulative = { attack: 0, vigor: 0, minorWound: 0, majorWound: 0, deadlyWound: 0 } + for (let i = 1; i <= sizeLevel; i++) { + const cfg = sizeLevels[i] || {} + context.sizeCumulative.attack += cfg.attack || 0 + context.sizeCumulative.vigor += cfg.vigor || 0 + context.sizeCumulative.minorWound += cfg.minorWound || 0 + context.sizeCumulative.majorWound += cfg.majorWound || 0 + context.sizeCumulative.deadlyWound += cfg.deadlyWound || 0 + } break case "combat": context.tab = context.tabs.combat @@ -77,4 +93,25 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet { } return context } + + /** @override - Les créatures n'ont pas de caractéristiques/compétences : on lance directement le pool d'attaque. */ + async _onRoll(event) { + event.preventDefault() + const el = event.currentTarget + const type = el.dataset.type + if (type !== "creature-attack") return super._onRoll(event) + const pool = this.document.system.computed?.attack ?? 0 + if (!pool) { + ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability")) + return + } + const difficulty = this.document.system.combatStatus?.difficulty ?? 7 + const { VermineUtils } = await import("../../system/roll.mjs") + await VermineUtils.roll({ + actor: this.document, + NoD: pool, + difficulty: parseInt(difficulty, 10), + rollLabel: game.i18n.localize("ADVERSITY.attack") + }) + } } diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs index 8c9a633..8107631 100644 --- a/module/applications/sheets/npc-sheet.mjs +++ b/module/applications/sheets/npc-sheet.mjs @@ -2,6 +2,10 @@ import VermineBaseActorSheet from "./base-actor-sheet.mjs" export default class VermineNpcSheetV2 extends VermineBaseActorSheet { + // La fiche PNJ s'ouvre en mode édition : caractéristiques, compétences et + // niveaux se règlent dès l'ouverture (le mode jeu reste accessible via le toggle). + _sheetMode = this.constructor.SHEET_MODES.EDIT + static DEFAULT_OPTIONS = { classes: ["npc"], position: { width: 750, height: 680 }, diff --git a/module/models/_shared.mjs b/module/models/_shared.mjs index d6c0c05..f8fbe0f 100644 --- a/module/models/_shared.mjs +++ b/module/models/_shared.mjs @@ -219,7 +219,7 @@ const reqInt = { required: true, nullable: false, integer: true } export function raritySchema() { const fields = foundry.data.fields return new fields.SchemaField({ - value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }), + value: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 10 }), handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }) }) } diff --git a/module/models/creature.mjs b/module/models/creature.mjs index 6f5a13b..486fb74 100644 --- a/module/models/creature.mjs +++ b/module/models/creature.mjs @@ -48,7 +48,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel // Niveaux de créature (patron, taille, rôle, meute) pattern: attributeSchema(1, 1, 4), - size: attributeSchema(1, 1, 3), + size: attributeSchema(0, 0, 3), role: attributeSchema(1, 1, 4), pack: attributeSchema(0, 0, 3), @@ -97,28 +97,38 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel */ _calculateCreatureComputedValues() { const patternLevel = this.pattern?.value || 1 - const sizeLevel = this.size?.value || 1 + const sizeLevel = this.size?.value || 0 const roleLevel = this.role?.value || 1 const packLevel = this.pack?.value || 0 const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {} - const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {} const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {} const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {} + // Les modificateurs de Taille se cumulent : une créature de Taille N applique + // les modificateurs de chaque Taille de 1 à N (Taille 0 = insecte, aucun). + const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {} + let sizeAttack = 0 + let sizeVigor = 0 + for (let i = 1; i <= sizeLevel; i++) { + const cfg = sizeLevels[i] || {} + sizeAttack += cfg.attack || 0 + sizeVigor += cfg.vigor || 0 + } + // Attaque : patron + taille + meute + réaction du rôle this.computed.attack = (patternConfig.attack || 0) - + (sizeConfig.attack || 0) + + sizeAttack + (packConfig.attack || 0) + (roleConfig.reaction || 0) // Dégâts : patron + vigueur de taille + meute this.computed.damage = (patternConfig.damage || 0) - + (sizeConfig.vigor || 0) + + sizeVigor + (packConfig.damage || 0) // Vigueur : taille + meute - this.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0) + this.computed.vigor = sizeVigor + (packConfig.damage || 0) // Réaction : rôle this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0) @@ -141,21 +151,32 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel */ _calculateCreatureWoundThresholds() { const patternLevel = this.pattern?.value || 1 - const sizeLevel = this.size?.value || 1 + const sizeLevel = this.size?.value || 0 const packLevel = this.pack?.value || 0 const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {} - const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {} const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {} + // Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N). + const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {} + let sizeMinor = 0 + let sizeMajor = 0 + let sizeDeadly = 0 + for (let i = 1; i <= sizeLevel; i++) { + const cfg = sizeLevels[i] || {} + sizeMinor += cfg.minorWound || 0 + sizeMajor += cfg.majorWound || 0 + sizeDeadly += cfg.deadlyWound || 0 + } + this.minorWound.threshold = (patternConfig.minorWound || 0) - + (sizeConfig.minorWound || 0) + + sizeMinor + (packConfig.minorWound || 0) this.majorWound.threshold = (patternConfig.majorWound || 0) - + (sizeConfig.majorWound || 0) + + sizeMajor + (packConfig.majorWound || 0) this.deadlyWound.threshold = (patternConfig.deadlyWound || 0) - + (sizeConfig.deadlyWound || 0) + + sizeDeadly + (packConfig.deadlyWound || 0) // Max de blessures diff --git a/module/models/specialty.mjs b/module/models/specialty.mjs index 1b0e205..e838be0 100644 --- a/module/models/specialty.mjs +++ b/module/models/specialty.mjs @@ -10,9 +10,11 @@ export default class VermineSpecialtyData extends foundry.abstract.TypeDataModel /** @override */ static defineSchema() { const fields = foundry.data.fields + const reqInt = { required: true, nullable: false, integer: true } return { name: new fields.StringField({ required: true, nullable: false, initial: "" }), - skill: new fields.StringField({ required: true, nullable: false, initial: "" }) + skill: new fields.StringField({ required: true, nullable: false, initial: "" }), + level: new fields.NumberField({ ...reqInt, initial: 1, min: 0, max: 5 }) } } } diff --git a/module/system/config.mjs b/module/system/config.mjs index 3abd57f..ccf16f8 100644 --- a/module/system/config.mjs +++ b/module/system/config.mjs @@ -139,9 +139,10 @@ VERMINE.creaturePatternLevels = { * Creature Size Levels configuration */ VERMINE.creatureSizeLevels = { - 1: { "label": "SIZE_LEVELS.small", "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 }, - 2: { "label": "SIZE_LEVELS.medium", "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 }, - 3: { "label": "SIZE_LEVELS.large", "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 } + 0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 }, + 1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 }, + 2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }, + 3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 } } /** diff --git a/module/system/dialogs/rollDialog.mjs b/module/system/dialogs/rollDialog.mjs index 68b1ff9..453ed90 100644 --- a/module/system/dialogs/rollDialog.mjs +++ b/module/system/dialogs/rollDialog.mjs @@ -112,18 +112,32 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli #getHandicap() { return this.#el.querySelector("#handicap"); } #getSelfCtrl() { return this.#el.querySelector("#self_control"); } - getDicePool() { + /** Décomposition du pool de dés pour affichage dans le message. */ + getPoolBreakdown() { const abil = this.#getAbility(); - const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0; + const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0; const skill = this.#getSkill(); const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0; - const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0; - const specChecked = this.#el.querySelector("#usingSpecialization")?.checked; + const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0; + const specChecked = this.hasSpecialtySelected(); const helped = this.#el.querySelector("#helped")?.checked; - const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0"; + const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked"); + const tools = toolsChecked && toolsChecked.value !== "0"; const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0; - const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0) + group; - return (abilVal + sc + skillPool + bonuses) || 0; + const specialty = specChecked ? 1 : 0; + const help = helped ? 1 : 0; + const tooling = tools ? 1 : 0; + const total = ability + skillPool + selfControl + specialty + help + tooling + group; + return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, total }; + } + + getDicePool() { + return this.getPoolBreakdown().total; + } + + getSpecialtyName() { + const checked = this.#el.querySelector("input[name='usingSpecialization']:checked"); + return checked && checked.value !== "aucune" ? checked.value : null; } getDifficultySelect() { @@ -207,14 +221,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli } #calculateBonusCount() { - let b = 0; - if (this.#el.querySelector("#helped")?.checked) b += 1; - b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0; - b += parseInt(this.#getSelfCtrl()?.value, 10) || 0; - const tools = this.#el.querySelector("input[name='usingTools']:checked"); - if (tools && tools.value !== "0") b += 1; - if (this.hasSpecialtySelected()) b += 1; - return b; + const b = this.getPoolBreakdown(); + return b.specialty + b.help + b.tooling + b.group + b.selfControl; } #updateUI() { @@ -315,7 +323,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli keepTotem: this.getKeepTotem(), skillCategory: this.getSkillCategory(), skillLevel: this.getSkillLevel(), - hasSpecialty: this.hasSpecialtySelected() + hasSpecialty: this.hasSpecialtySelected(), + poolBreakdown: this.getPoolBreakdown(), + specialtyName: this.getSpecialtyName() }); this.close(); diff --git a/module/system/handlebars-manager.mjs b/module/system/handlebars-manager.mjs index f161dfd..7bfc732 100644 --- a/module/system/handlebars-manager.mjs +++ b/module/system/handlebars-manager.mjs @@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () { // npc partials "systems/vermine2047/templates/actor/npc/npc-combat.hbs", "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs", + "systems/vermine2047/templates/actor/parts/npc-skill-item.hbs", // creature partials "systems/vermine2047/templates/actor/creature/creature-combat.hbs", @@ -100,6 +101,21 @@ export const registerHandlebarsHelpers = function () { return game.i18n.localize(arrayLabel + "." + objectLabel + ".name"); }); + // Return an object of skills ordered alphabetically by their localized + // display name (easier to find a skill quickly). Use with `{{#each}}`. + Handlebars.registerHelper('sortedSkills', function (skills) { + if (!skills || typeof skills !== 'object') return skills; + const sorted = {}; + Object.keys(skills) + .sort((a, b) => { + const na = game.i18n.localize(`SKILLS.${a}.name`).toLocaleLowerCase(); + const nb = game.i18n.localize(`SKILLS.${b}.name`).toLocaleLowerCase(); + return na.localeCompare(nb, game.i18n.lang); + }) + .forEach(k => { sorted[k] = skills[k]; }); + return sorted; + }); + Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) { return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key); }); @@ -347,6 +363,9 @@ export const registerHandlebarsHelpers = function () { Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) { return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this); }); + Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) { + return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this); + }); //math operations diff --git a/module/system/roll.mjs b/module/system/roll.mjs index e3d768d..0677c65 100644 --- a/module/system/roll.mjs +++ b/module/system/roll.mjs @@ -29,7 +29,9 @@ export class VermineUtils { keepTotem = null, skillLevel = null, hasSpecialty = false, - handicap = 0 + handicap = 0, + poolBreakdown = null, + specialtyName = null }) { // Validate inputs if (!actor) { @@ -105,6 +107,27 @@ export class VermineUtils { // Build final formula formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula; + // Reconcile the breakdown displayed in chat with the dice actually rolled. + // Domain bonuses/malus are only applied when the corresponding totem die + // is used, and totem dice replace (not add to) base-pool dice. + if (poolBreakdown) { + // Count totem dice present in the final modifier formula. + let totemDice = 0; + if (modFormula) { + if (modFormula.includes("human_")) totemDice += 1; + if (modFormula.includes("adapted_")) totemDice += 1; + } + // Only count the domain bonus of totems actually activated in the roll. + const domainBonus = (totems.human ? totemBonus.human : 0) + + (totems.adapted ? totemBonus.adapted : 0); + const basePool = poolBreakdown.total; + poolBreakdown.totemDice = totemDice; + poolBreakdown.domainBonus = domainBonus; + // Total = nombre réel de dés lancés (NoD réguliers + dés de totem). + // Équivaut à basePool + domainBonus sauf si le pool régulier clampe à 0. + poolBreakdown.total = NoD + totemDice; + } + // Create the roll const roll = new Roll(formula, actor.getRollData()); @@ -143,7 +166,9 @@ export class VermineUtils { keepTotem, skillLevel, hasSpecialty, - handicap + handicap, + poolBreakdown, + specialtyName }); return roll; @@ -199,10 +224,10 @@ export class VermineUtils { /** * Handles reroll events on dice in chat messages. * @param {Object} message - The chat message containing the reroll event - * @param {Object} ev - The reroll event + * @param {HTMLElement} target - The clicked die element * @returns {Promise} Whether the reroll was successful */ - static async onReroll(message, ev) { + static async onReroll(message, target) { // Verify user permissions const msgUserId = message.user?.id ?? message.user; if (msgUserId !== game.user?.id && !game.user?.isGM) { @@ -211,7 +236,7 @@ export class VermineUtils { } // Get reroll count - const rollMessage = ev.currentTarget.closest('div.vermine-roll-message'); + const rollMessage = target.closest('div.vermine-roll-message'); if (!rollMessage) { return false; } @@ -221,22 +246,22 @@ export class VermineUtils { // Check if rerolls are available if (!rerollCount || parseInt(rerollCount, 10) < 1) { ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left')); - const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable'); + const rerollables = target.closest('ul')?.querySelectorAll('.rerollable'); if (rerollables) { rerollables.forEach(el => el.classList.remove('rerollable')); } return false; } - ev.currentTarget.classList.add('rerolled'); + target.classList.add('rerolled'); // Set reroll flag await message.setFlag("world", "reroll", true); // Get difficulty and dice type - const ulElement = ev.currentTarget.closest('ul'); + const ulElement = target.closest('ul'); const difficulty = ulElement?.dataset.difficulty ?? 7; - let diceType = ev.currentTarget.dataset.diceType; + let diceType = target.dataset.diceType; // Sanitize user name const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_'); @@ -265,30 +290,68 @@ export class VermineUtils { // Update die display const result = reroll.dice[0]?.results[0]?.result ?? 0; - const dieSpan = ev.currentTarget.querySelector('span'); + const dieSpan = target.querySelector('span'); if (dieSpan) { dieSpan.innerText = result; } + // Update the face used for the CSS background image. + target.dataset.result = result; - // Update total if successful + // Set/clear the success class from the rerolled die (a previous success + // must be removed if the new result fails). const success = reroll.dice[0]?.results[0]?.success; - if (success) { - ev.currentTarget.classList.add('success'); - const totalElement = rollMessage.querySelector('#total'); - if (totalElement) { - const currentTotal = parseInt(totalElement.innerText, 10) || 0; - totalElement.innerText = currentTotal + reroll.total; - } + target.classList.toggle('success', !!success); + target.classList.remove("rerollable"); + + // Recompute the total from the live dice in the DOM. This avoids the + // cumulative-increment drift of the old logic (which never removed a + // removed success from #total). Totem dice (human/adapted) count double. + const totalEl = rollMessage.querySelector('#total'); + if (totalEl) { + let total = 0; + rollMessage.querySelectorAll('li.die.success').forEach(die => { + const isTotem = die.classList.contains('human') || die.classList.contains('adapted'); + total += isTotem ? 2 : 1; + }); + totalEl.innerText = total; } // Update message content - ev.currentTarget.classList.remove("rerollable"); - - const messageContent = ev.currentTarget.closest('div.message-content'); + const messageContent = target.closest('div.message-content'); if (messageContent) { const newRerollCount = parseInt(rerollCount, 10) - 1; rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount; - + + // Recompute the verdict banner from the recomputed total. + const verdictEl = rollMessage.querySelector('.roll-verdict'); + const requiredEl = rollMessage.querySelector('#required'); + if (verdictEl && totalEl && requiredEl) { + const total = parseInt(totalEl.innerText, 10) || 0; + const required = parseInt(requiredEl.innerText, 10) || 1; + const won = total >= required; + verdictEl.classList.toggle('success', won); + verdictEl.classList.toggle('failure', !won); + const label = won + ? game.i18n.localize('VERMINE.roll_success') + : game.i18n.localize('VERMINE.roll_failure'); + verdictEl.innerHTML = `${won ? '' : ''}${label}`; + } + + // Recompute the dice summary from the current die list. + const summaryEl = rollMessage.querySelector('.roll-dice-summary'); + if (summaryEl) { + const stats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } }; + rollMessage.querySelectorAll('li.die').forEach(die => { + const type = die.classList.contains('human') ? 'human' : die.classList.contains('adapted') ? 'adapted' : 'regular'; + stats[type].count += 1; + if (die.classList.contains('success')) stats[type].success += 1; + }); + const mk = (key, count, s) => count ? `${count} ${game.i18n.localize(`VERMINE.die_${key}`)} · ${s} ${game.i18n.localize('VERMINE.die_successes')}` : ''; + summaryEl.innerHTML = mk('regular', stats.regular.count, stats.regular.success) + + mk('human', stats.human.count, stats.human.success) + + mk('adapted', stats.adapted.count, stats.adapted.success); + } + await message.update({ content: messageContent.outerHTML }); @@ -314,10 +377,11 @@ export class VermineUtils { html.querySelectorAll('.rerollable').forEach(el => { el.addEventListener('click', async (ev) => { ev.preventDefault(); - const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId; + const dieEl = ev.currentTarget; + const msgId = dieEl.closest("li.message")?.dataset?.messageId; if (msgId) { const message = await game.messages.get(msgId); - await VermineUtils.onReroll(message, ev); + await VermineUtils.onReroll(message, dieEl); } }); }); @@ -336,6 +400,7 @@ export class VermineUtils { // Add click event for granting rerolls html.querySelectorAll("button.grant-reroll").forEach(el => { el.addEventListener('click', async (ev) => { + const btn = ev.currentTarget; const grantedRerollElement = html.querySelector('#granted-reroll'); const allowedRerollElement = html.querySelector("#allowed_reroll"); @@ -343,13 +408,13 @@ export class VermineUtils { allowedRerollElement.innerText = grantedRerollElement.innerText; } - const mesEl = ev.currentTarget.closest('[data-message-id]'); + const mesEl = btn.closest('[data-message-id]'); const messageId = mesEl?.dataset?.messageId; if (messageId) { - ev.currentTarget.closest('.reroll-from-effort').style.display = "none"; + btn.closest('.reroll-from-effort').style.display = "none"; - const rollMessage = ev.currentTarget.closest(".vermine-roll-message"); + const rollMessage = btn.closest(".vermine-roll-message"); if (rollMessage) { const content = rollMessage.outerHTML; const message = await game.messages.get(messageId); @@ -401,7 +466,28 @@ export class VermineUtils { * @returns {Promise} The created chat message */ static async diplayChatRoll(roll, param) { - const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param }); + // Aggregate dice stats per type (regular / human / adapted) + const diceStats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } }; + for (const die of roll.dice ?? []) { + // Flavors look like "_"; classify from the leading token. + const flavor = die.options?.flavor ?? ""; + const prefix = flavor.split("_")[0]; + const type = prefix === "human" ? "human" : prefix === "adapted" ? "adapted" : "regular"; + for (const r of die.results ?? []) { + diceStats[type].count += 1; + if (r.success) diceStats[type].success += 1; + } + } + // Verdict + const required = 1 + (param.handicap ?? 0); + const total = roll._total ?? 0; + const verdict = { + success: total >= required, + total, + required + }; + + const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict }); const chatData = { user: game.user?._id, speaker: ChatMessage.getSpeaker(), diff --git a/templates/actor/actor-npc-sheet.hbs b/templates/actor/actor-npc-sheet.hbs index 470cd3e..0be36bf 100644 --- a/templates/actor/actor-npc-sheet.hbs +++ b/templates/actor/actor-npc-sheet.hbs @@ -221,6 +221,7 @@ {{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs" categoryKey=key categoryLabel=(concat "VERMINE.skill_category." key) + skills=@root.system.skills }} {{/if}} {{/each}} diff --git a/templates/actor/appv2/character-abilities.hbs b/templates/actor/appv2/character-abilities.hbs index 772c7a8..f49104a 100644 --- a/templates/actor/appv2/character-abilities.hbs +++ b/templates/actor/appv2/character-abilities.hbs @@ -55,7 +55,7 @@ > {{/if}} - {{#each @root.system.skills as |skill skey|}} + {{#each (sortedSkills @root.system.skills) as |skill skey|}} {{#if (eq skill.category sckey) }}
@@ -79,12 +79,10 @@ data-action="addSpecialty" > {{/ifgt}} - {{#each skill.specialties as |spe ind|}} + {{#each @root.specialties as |spe|}} {{#ife spe.system.skill skey}} - {{spe.name}} + {{spe.name}}{{spe.system.level}} {{/ife}} {{/each}}
diff --git a/templates/actor/appv2/character-equipment.hbs b/templates/actor/appv2/character-equipment.hbs index a123ff1..50e2fec 100644 --- a/templates/actor/appv2/character-equipment.hbs +++ b/templates/actor/appv2/character-equipment.hbs @@ -1,90 +1,4 @@
-
-
-
    -
  1. -
    {{ localize - 'IDENTITY.name'}}
    -
    {{ localize 'VERMINE.qty'}}
    -
    {{ localize 'VERMINE.weight'}}
    -
    - {{#if isEditMode}} - - {{/if}} -
    -
  2. - {{#each gear as |item id|}} -
  3. -
    -
    - -
    - {{item.name}} - - {{#if item.system.damages.value}} - / - {{{item.damagedIcon}}} - {{/if}} - -
    - - -
    - {{#if @root.isEditMode}} - - {{/if}} -
    -
  4. - {{/each}} -
-
-
-

{{ localize 'IDENTITY.notes'}}

- {{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}} -
-

{{localize 'ITEMS.weapons'}}

{{> "systems/vermine2047/templates/actor/parts/actor-weapons.hbs"}} @@ -93,4 +7,87 @@

{{localize 'ITEMS.defenses'}}

{{> "systems/vermine2047/templates/actor/parts/actor-defenses.hbs"}}
+
+

{{localize 'ITEMS.items'}}

+
    +
  1. +
    {{ localize + 'IDENTITY.name'}}
    +
    {{ localize 'VERMINE.qty'}}
    +
    {{ localize 'VERMINE.weight'}}
    +
    + +
    +
  2. + {{#each gear as |item id|}} +
  3. +
    +
    + +
    + {{item.name}} + + {{#if item.system.damages.value}} + / + {{{item.damagedIcon}}} + {{/if}} + +
    + + +
    + {{#if @root.isEditMode}} + + {{/if}} +
    +
  4. + {{/each}} +
+
+
+

{{ localize 'IDENTITY.notes'}}

+ {{formInput systemFields.equipment.fields.description enriched=enrichedEquipmentNotes value=system.equipment.description name="system.equipment.description" toggled=true}} +
diff --git a/templates/actor/appv2/creature-stats.hbs b/templates/actor/appv2/creature-stats.hbs index 4d097d4..a63969d 100644 --- a/templates/actor/appv2/creature-stats.hbs +++ b/templates/actor/appv2/creature-stats.hbs @@ -1,5 +1,12 @@

{{ localize 'VERMINE.base_values' }}

+ + {{!-- Total attaque (lancable) --}} +
+

{{ localize 'VERMINE.total_attack' }}

+ {{ system.computed.attack }} +
+

{{ localize 'ADVERSITY.pattern' }}: {{ patternLabel }}

@@ -16,12 +23,12 @@

{{ localize 'ADVERSITY.size' }}: {{ sizeLabel }}

    -
  • {{ localize 'ADVERSITY.attack' }}: {{ creatureSizeLevel "attack" system.size.value }}
  • -
  • {{ localize 'ADVERSITY.vigor' }}: {{ creatureSizeLevel "vigor" system.size.value }}
  • +
  • {{ localize 'ADVERSITY.attack' }}: {{ sizeCumulative.attack }}
  • +
  • {{ localize 'ADVERSITY.vigor' }}: {{ sizeCumulative.vigor }}
  • {{ localize 'ADVERSITY.wounds' }}: - {{ creatureSizeLevel "minorWound" system.size.value }}/ - {{ creatureSizeLevel "majorWound" system.size.value }}/ - {{ creatureSizeLevel "deadlyWound" system.size.value }} + {{ sizeCumulative.minorWound }}/ + {{ sizeCumulative.majorWound }}/ + {{ sizeCumulative.deadlyWound }}
diff --git a/templates/actor/appv2/npc-characteristics.hbs b/templates/actor/appv2/npc-characteristics.hbs index a8250e6..3751cc8 100644 --- a/templates/actor/appv2/npc-characteristics.hbs +++ b/templates/actor/appv2/npc-characteristics.hbs @@ -6,7 +6,7 @@

{{ localize 'VERMINE.ability_category.physical' }}

- +
{{#if isEditMode}} @@ -15,7 +15,7 @@
- +
{{#if isEditMode}} @@ -31,7 +31,7 @@

{{ localize 'VERMINE.ability_category.manual' }}

- +
{{#if isEditMode}} @@ -40,7 +40,7 @@
- +
{{#if isEditMode}} @@ -56,7 +56,7 @@

{{ localize 'VERMINE.ability_category.mental' }}

- +
{{#if isEditMode}} @@ -65,7 +65,7 @@
- +
{{#if isEditMode}} @@ -81,7 +81,7 @@

{{ localize 'VERMINE.ability_category.social' }}

- +
{{#if isEditMode}} @@ -90,7 +90,7 @@
- +
{{#if isEditMode}} diff --git a/templates/actor/appv2/npc-skills.hbs b/templates/actor/appv2/npc-skills.hbs index 7260e37..f9c207d 100644 --- a/templates/actor/appv2/npc-skills.hbs +++ b/templates/actor/appv2/npc-skills.hbs @@ -27,6 +27,7 @@ {{> "systems/vermine2047/templates/actor/parts/npc-skill-category.hbs" categoryKey=key categoryLabel=(concat "VERMINE.skill_category." key) + skills=@root.system.skills }} {{/ife}} {{/each}} diff --git a/templates/actor/character/character-features.hbs b/templates/actor/character/character-features.hbs index 6645a1e..412bc4e 100644 --- a/templates/actor/character/character-features.hbs +++ b/templates/actor/character/character-features.hbs @@ -48,7 +48,7 @@ {{/ife}} > - {{#each @root.system.skills as |skill skey|}} + {{#each (sortedSkills @root.system.skills) as |skill skey|}} {{#if (eq skill.category sckey) }}
@@ -70,14 +70,16 @@ data-tooltip="ajout de spécialité" > {{/ifgt}} - {{#each skill.specialties as |spe ind|}} + {{#if @root.specialties}} + {{#each @root.specialties as |spe|}} {{#ife spe.system.skill skey}} - {{spe.name}} + >{{spe.name}}{{spe.system.level}} {{/ife}} {{/each}} + {{/if}}
- {{#each @root.config.skillCategories as |skillCategory sckey|}} - - {{#each @root.config.skills as |skill key|}} - {{#ife skill.category sckey}} - - {{/ife}} - {{/each}} - - {{/each}} - +
+ + +
diff --git a/templates/roll-message.hbs b/templates/roll-message.hbs index 9f572ca..9c8cd14 100644 --- a/templates/roll-message.hbs +++ b/templates/roll-message.hbs @@ -1,11 +1,99 @@
-

{{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}}

- -
-

{{localize "VERMINE.difficulty"}}:

- {{param.difficulty}} + {{!-- Verdict --}} +
+ {{#if verdict.success}} + + {{localize "VERMINE.roll_success"}} + {{else}} + + {{localize "VERMINE.roll_failure"}} + {{/if}}
+

{{param.actor.name}} : {{localize "VERMINE.test_of"}} {{param.rollLabel}}

+ + {{!-- Context --}} +
+
+ {{localize "VERMINE.difficulty"}} + {{param.difficulty}} +
+
+ {{localize "VERMINE.success_required"}} + {{verdict.required}} +
+ {{#if param.handicap}} +
+ {{localize "VERMINE.handicap"}} + +{{param.handicap}} +
+ {{/if}} + {{#if param.specialtyName}} +
+ {{localize "VERMINE.specialty"}} + +1D {{param.specialtyName}} +
+ {{/if}} + {{#if param.self_control}} +
+ {{localize "VERMINE.self_control"}} + +{{param.self_control}}D +
+ {{/if}} + {{#if param.totems.human}} +
+ {{localize "VERMINE.totem_used"}} + {{localize "TOTEMS.human.name"}}{{#ife param.keepTotem "human"}} ({{localize "VERMINE.totem_keep"}}){{/ife}} +
+ {{/if}} + {{#if param.totems.adapted}} +
+ {{localize "VERMINE.totem_used"}} + {{localize "TOTEMS.adapted.name"}}{{#ife param.keepTotem "adapted"}} ({{localize "VERMINE.totem_keep"}}){{/ife}} +
+ {{/if}} + {{#if param.skillLevel}} +
+ {{localize "VERMINE.skill_mastery"}} + {{skillLevel "label" param.skillLevel}} +
+ {{/if}} +
+ + {{!-- Dice pool breakdown --}} + {{#if param.poolBreakdown}} +
+

{{localize "VERMINE.roll_pool"}}

+
+ {{#if param.poolBreakdown.ability}} + {{localize "VERMINE.pool_ability"}} {{param.poolBreakdown.ability}}D + {{/if}} + {{#if param.poolBreakdown.skill}} + {{localize "VERMINE.pool_skill"}} {{param.poolBreakdown.skill}}D + {{/if}} + {{#if param.poolBreakdown.specialty}} + {{localize "VERMINE.pool_specialty"}} +{{param.poolBreakdown.specialty}}D + {{/if}} + {{#if param.poolBreakdown.help}} + {{localize "VERMINE.pool_help"}} +{{param.poolBreakdown.help}}D + {{/if}} + {{#if param.poolBreakdown.tooling}} + {{localize "VERMINE.pool_tooling"}} +{{param.poolBreakdown.tooling}}D + {{/if}} + {{#if param.poolBreakdown.group}} + {{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D + {{/if}} + {{#if param.poolBreakdown.selfControl}} + {{localize "VERMINE.pool_selfControl"}} +{{param.poolBreakdown.selfControl}}D + {{/if}} + {{#if param.poolBreakdown.domainBonus}} + {{#ifgt param.poolBreakdown.domainBonus 0}}{{localize "VERMINE.domain_bonus"}}{{else}}{{localize "VERMINE.domain_penalty"}}{{/ifgt}} {{param.poolBreakdown.domainBonus}}D + {{/if}} + {{localize "VERMINE.pool_total"}} {{param.poolBreakdown.total}}D +
+
+ {{/if}} +

{{localize "VERMINE.rerolls_possible"}} : {{param.Reroll}}

@@ -25,21 +113,22 @@
- + + {{!-- Dice results --}}
    {{#each roll.dice as |dieType index|}} {{#each dieType.results as |die index|}} - {{! Determine dice class and type based on flavor }} - {{#ifincludes dieType.options.flavor "human"}} + {{! Determine dice class and type based on flavor (leading token) }} + {{#ifStartsWith dieType.options.flavor "human_"}} {{set diceClass="human"}} {{set diceTypeVal="human"}} - {{else ifincludes dieType.options.flavor "adapted"}} + {{else ifStartsWith dieType.options.flavor "adapted_"}} {{set diceClass="adapted"}} {{set diceTypeVal="adapted"}} {{else}} {{set diceClass="regular"}} {{set diceTypeVal="regular"}} - {{/ifincludes}} + {{/ifStartsWith}}
  • @@ -48,7 +137,21 @@ {{/each}} {{/each}}
- + + {{!-- Dice summary --}} +
+ {{#if diceStats.regular.count}} + {{diceStats.regular.count}} {{localize "VERMINE.die_regular"}} · {{diceStats.regular.success}} {{localize "VERMINE.die_successes"}} + {{/if}} + {{#if diceStats.human.count}} + {{diceStats.human.count}} {{localize "VERMINE.die_human"}} · {{diceStats.human.success}} {{localize "VERMINE.die_successes"}} + {{/if}} + {{#if diceStats.adapted.count}} + {{diceStats.adapted.count}} {{localize "VERMINE.die_adapted"}} · {{diceStats.adapted.success}} {{localize "VERMINE.die_successes"}} + {{/if}} +
+ + {{!-- Total --}}

{{localize "VERMINE.success_count"}}:

@@ -56,7 +159,7 @@

{{localize "VERMINE.success_required"}}:

- {{math_add 1 param.handicap}} + {{verdict.required}}
-
\ No newline at end of file +