Mise à jour des compendiums et scripts pour v14

- Mise à jour des manifestes et logs des packs
- Modification des scripts NPC (NpcDialog.js, travellerNpcGenerator.js, npc.js)
- Mise à jour de la description du module pour refléter l'onglet 'PNJ Détaillé'

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-05-28 00:47:11 +02:00
parent 76870c27bf
commit 9453c15d58
87 changed files with 581 additions and 390 deletions
+174 -2
View File
@@ -1,6 +1,19 @@
import { formatCredits } from './tradeHelper.js';
import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc } from './npcHelper.js';
import { NPC_RELATIONS } from './data/npcTables.js';
import { generateAndCreateTravellerNpc } from './travellerNpcGenerator.js';
import { generateRandomName } from './data/travellerNpcGenerator.js';
import {
CITIZEN_CATEGORY_LIST,
EXPERIENCE_LEVEL_LIST,
ROLE_LIST,
GENDER_LIST,
DEFAULT_OPTIONS,
CITIZEN_CATEGORY_LABELS_FR,
EXPERIENCE_LEVEL_LABELS_FR,
ROLE_LABELS_FR,
GENDER_LABELS_FR
} from './data/travellerNpcGenerator.js';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
@@ -41,6 +54,19 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
context: options.context ?? 'starport',
includeFollowUp: true,
},
mission: {},
traveller: {
citizenCategory: DEFAULT_OPTIONS.citizenCategory,
experience: DEFAULT_OPTIONS.experience,
role: DEFAULT_OPTIONS.role,
gender: DEFAULT_OPTIONS.gender,
firstName: '',
surname: '',
useRandomName: true,
createActor: DEFAULT_OPTIONS.createActor,
actorName: '',
openCreatedActor: DEFAULT_OPTIONS.openCreatedActor,
},
};
}
@@ -50,6 +76,25 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
...this._formData,
activeTab: this._activeTab,
relations: Object.entries(NPC_RELATIONS).map(([key, value]) => ({ key, label: value.label })),
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
key: c.key,
label: CITIZEN_CATEGORY_LABELS_FR[c.key] || c.label,
description: c.description
})),
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
key: e.key,
label: EXPERIENCE_LEVEL_LABELS_FR[e.key] || e.label,
description: e.description
})),
roles: ROLE_LIST.map(r => ({
key: r.key,
label: ROLE_LABELS_FR[r.key] || r.label,
description: r.description
})),
genders: GENDER_LIST.map(g => ({
key: g.key,
label: GENDER_LABELS_FR[g.key] || g.label
})),
};
}
@@ -84,6 +129,27 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._readForm(html);
this._activateTab($(event.currentTarget).data('tab'));
});
// Gestion des événements pour l'onglet PNJ Détaillé (Traveller)
html.find('[data-action="generate-traveller-npc"]').on('click', async (event) => {
event.preventDefault();
this._readForm(html);
await this._handleTravellerNpc();
});
html.find('[data-action="randomize-name"]').on('click', (event) => {
event.preventDefault();
this._randomizeTravellerName(html);
});
html.find('[name="traveller.useRandomName"]').on('change', (event) => {
const useRandom = event.target.checked;
this._formData.traveller.useRandomName = useRandom;
html.find('.traveller-name-fields').toggleClass('hidden', useRandom);
});
// Initialiser l'affichage des champs de nom pour l'onglet Traveller
html.find('.traveller-name-fields').toggleClass('hidden', this._formData.traveller.useRandomName);
}
_getForm() {
@@ -137,6 +203,18 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._formData.npc.openCreatedActor = html.find('[name="npc.openCreatedActor"]').is(':checked');
this._formData.encounter.context = html.find('[name="encounter.context"]').val();
this._formData.encounter.includeFollowUp = html.find('[name="encounter.includeFollowUp"]').is(':checked');
// Données pour l'onglet PNJ Détaillé (Traveller)
this._formData.traveller.citizenCategory = html.find('[name="traveller.citizenCategory"]').val();
this._formData.traveller.experience = html.find('[name="traveller.experience"]').val();
this._formData.traveller.role = html.find('[name="traveller.role"]').val();
this._formData.traveller.gender = html.find('[name="traveller.gender"]').val();
this._formData.traveller.firstName = html.find('[name="traveller.firstName"]').val();
this._formData.traveller.surname = html.find('[name="traveller.surname"]').val();
this._formData.traveller.useRandomName = html.find('[name="traveller.useRandomName"]').is(':checked');
this._formData.traveller.createActor = html.find('[name="traveller.createActor"]').is(':checked');
this._formData.traveller.actorName = html.find('[name="traveller.actorName"]').val();
this._formData.traveller.openCreatedActor = html.find('[name="traveller.openCreatedActor"]').is(':checked');
}
async _handleNpc() {
@@ -162,14 +240,75 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
await this._postToChatResult(result);
}
async _handleTravellerNpc() {
const button = $(this.element).find('[data-action="generate-traveller-npc"]');
const originalLabel = button.html();
try {
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Génération...');
const generateOptions = {
citizenCategory: this._formData.traveller.citizenCategory,
experience: this._formData.traveller.experience,
role: this._formData.traveller.role,
gender: this._formData.traveller.gender,
createActor: this._formData.traveller.createActor,
actorName: this._formData.traveller.actorName,
openCreatedActor: this._formData.traveller.openCreatedActor
};
if (!this._formData.traveller.useRandomName && this._formData.traveller.firstName && this._formData.traveller.surname) {
generateOptions.firstName = this._formData.traveller.firstName;
generateOptions.surname = this._formData.traveller.surname;
}
const result = await generateAndCreateTravellerNpc(generateOptions);
if (result.success) {
await this._postToChatResult(result);
if (result.createdActor) {
ui.notifications.info(`Fiche PNJ Traveller créée : ${result.createdActor.name}`);
}
} else {
ui.notifications.error('Erreur lors de la génération du PNJ Traveller');
}
} catch (error) {
console.error(`${MODULE_ID} | Erreur lors de la génération du PNJ Traveller:`, error);
ui.notifications.error(`Erreur: ${error.message}`);
} finally {
button.prop('disabled', false).html(originalLabel);
}
}
_randomizeTravellerName(html) {
const name = generateRandomName(this._formData.traveller.gender);
html.find('[name="traveller.firstName"]').val(name.firstName);
html.find('[name="traveller.surname"]').val(name.surname);
this._formData.traveller.firstName = name.firstName;
this._formData.traveller.surname = name.surname;
this._formData.traveller.useRandomName = false;
html.find('[name="traveller.useRandomName"]').prop('checked', false);
html.find('.traveller-name-fields').removeClass('hidden');
}
async _postToChatResult(data) {
registerHandlebarsHelpers();
const html = await foundry.applications.handlebars.renderTemplate(`modules/${MODULE_ID}/templates/npc-result.hbs`, data);
// Déterminer quel template utiliser en fonction du type de données
let template = `modules/${MODULE_ID}/templates/npc-result.hbs`;
let resultType = 'npc-result';
if (data.type === 'traveller-npc' || data?.flags?.[MODULE_ID]?.type === 'traveller-npc-result') {
template = `modules/${MODULE_ID}/templates/traveller-npc-result.hbs`;
resultType = 'traveller-npc-result';
}
const html = await foundry.applications.handlebars.renderTemplate(template, data);
await ChatMessage.create({
content: html,
speaker: ChatMessage.getSpeaker(),
flags: { [MODULE_ID]: { type: 'npc-result' } },
flags: { [MODULE_ID]: { type: resultType } },
});
}
}
@@ -180,8 +319,41 @@ function registerHandlebarsHelpers() {
if (helpersRegistered) return;
helpersRegistered = true;
// Helpers existants pour NPC
Handlebars.registerHelper('eq', (a, b) => a === b);
Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : ''));
Handlebars.registerHelper('formatCredits', (amount) => formatCredits(amount));
Handlebars.registerHelper('contains', (text, search) => String(text ?? '').includes(search));
// Helpers pour Traveller NPC
Handlebars.registerHelper('gt', (a, b) => a > b);
// Helper pour afficher le niveau de compétence avec un symbole
Handlebars.registerHelper('skillLevelSymbol', (level) => {
if (level === 0) return '';
if (level === 1) return '★';
if (level === 2) return '★★';
if (level === 3) return '★★★';
return `+${level}`;
});
// Helper pour formater le DM (Difficulté Modificateur)
Handlebars.registerHelper('formatDm', (value) => {
const dm = Math.floor((value - 6) / 3);
return dm >= 0 ? `+${dm}` : `${dm}`;
});
// Helper pour obtenir la classe CSS du niveau de compétence
Handlebars.registerHelper('skillLevelClass', (level) => {
if (level === 3) return 'skill-level-3';
if (level === 2) return 'skill-level-2';
if (level === 1) return 'skill-level-1';
return 'skill-level-0';
});
// Helper pour lookup dans un objet
Handlebars.registerHelper('lookup', (obj, key) => {
if (!obj || !key) return '';
return obj[key] !== undefined ? obj[key] : '';
});
}
+2 -2
View File
@@ -774,7 +774,7 @@ export const DEFAULT_OPTIONS = {
*/
export const SKILL_LABELS_FR = {
'Pilot-Spacecraft': 'Pilote Vaisseau spatial',
'Pilot-Small Craft': 'Pilote Aéronef léger',
'Pilot-Small Craft': 'Pilote Petits vaisseaux',
'Pilot': 'Pilote',
'Flyer': 'Pilote Aéronef atmosphérique',
'Astrogation': 'Astrogation',
@@ -804,7 +804,7 @@ export const SKILL_LABELS_FR = {
'Advocate': 'Plaidoyer',
'Diplomat': 'Diplomatie',
'Streetwise': 'Rues',
'Leadership': 'Direction',
'Leadership': 'Leadership',
'Science-Biology': 'Science Biologie',
'Science-Chemistry': 'Science Chimie',
'Science': 'Science',
+1 -38
View File
@@ -1,5 +1,4 @@
import { NpcDialog } from './NpcDialog.js';
import { openTravellerNpcDialog } from './TravellerNpcDialog.js';
import { syncNpcRollTables } from './npcRollTableSync.js';
import './mgt2eMigration.js';
@@ -10,10 +9,6 @@ function openNpcDialog(initialTab, options = {}) {
new NpcDialog({ initialTab, ...options }).render({ force: true });
}
function openTravellerNpcGenerator() {
openTravellerNpcDialog();
}
function registerNpcCommand(commandName, initialTab) {
if (!ChatLogV2?.CHAT_COMMANDS) {
console.warn(`${MODULE_ID} | ChatLog.CHAT_COMMANDS indisponible, commande /${commandName} non enregistrée`);
@@ -30,22 +25,6 @@ function registerNpcCommand(commandName, initialTab) {
console.log(`${MODULE_ID} | Commande /${commandName} enregistrée via ChatLog.CHAT_COMMANDS`);
}
function registerTravellerNpcCommand() {
if (!ChatLogV2?.CHAT_COMMANDS) {
console.warn(`${MODULE_ID} | ChatLog.CHAT_COMMANDS indisponible, commande /gennpc non enregistrée`);
return;
}
ChatLogV2.CHAT_COMMANDS.gennpc = {
rgx: new RegExp(`^\\/gennpc(?:\\s+(.*))?$`, 'i'),
fn: () => {
openTravellerNpcGenerator();
return false;
},
};
console.log(`${MODULE_ID} | Commande /gennpc enregistrée via ChatLog.CHAT_COMMANDS`);
}
Hooks.once('init', () => {
console.log(`${MODULE_ID} | Outils PNJ initialisés`);
@@ -64,12 +43,11 @@ Hooks.once('init', () => {
registerNpcCommand('pnj', 'npc');
registerNpcCommand('rencontre', 'encounter');
registerNpcCommand('mission', 'mission');
registerTravellerNpcCommand();
});
Hooks.once('ready', async () => {
await syncNpcRollTables();
console.log(`${MODULE_ID} | Outils PNJ prêts tapez /pnj, /rencontre, /mission ou /gennpc dans le chat`);
console.log(`${MODULE_ID} | Outils PNJ prêts tapez /pnj, /rencontre ou /mission dans le chat`);
});
/**
@@ -115,11 +93,6 @@ Hooks.on('renderChatInput', (app, html, data) => {
event.stopImmediatePropagation();
openNpcDialog('mission');
input.val('');
} else if (content?.startsWith('/gennpc')) {
event.preventDefault();
event.stopImmediatePropagation();
openTravellerNpcGenerator();
input.val('');
}
}
});
@@ -147,11 +120,6 @@ Hooks.on('preCreateChatMessage', (message, data, options) => {
openNpcDialog('mission');
return false; // Empêche la création du message
}
if (content === '/gennpc' || content?.startsWith('/gennpc ')) {
openTravellerNpcGenerator();
return false; // Empêche la création du message
}
});
// Gardé pour compatibilité v13
@@ -186,9 +154,4 @@ Hooks.on('chatMessage', (...args) => {
openNpcDialog('mission');
return false;
}
if (trimmed === '/gennpc' || trimmed?.startsWith('/gennpc ')) {
openTravellerNpcGenerator();
return false;
}
});
+75 -27
View File
@@ -49,61 +49,100 @@ const MODULE_ID = 'mgt2-compendium-amiral-denisov';
* Mapping des compétences Traveller vers mgt2e
* @type {Object<string, string>}
*/
// Mapping des compétences Traveller vers MgT2e
// IMPORTANT: MgT2e utilise des noms de compétences EN MINUSCULES (ex: pilot, electronics, gunner)
// basé sur les références dans npcHelper.js et mgt2eSkills.js
// Format: 'Compétence-Traveller' -> 'competence_mgt2e' ou 'competence_mgt2e.specialite'
// Les spécialités sont en minuscules avec underscores
// Si une spécialité n'existe pas dans MgT2e, setSkillLevel appliquera le niveau à la compétence parente
const SKILL_MAPPING = {
'Pilot-Spacecraft': 'pilot.spacecraft',
'Pilot-Small Craft': 'pilot.smallcraft',
// Pilotage - MgT2e a une compétence "pilot" (confirmé par les références)
// Corrigé : Small Craft = Petits vaisseaux (pas Aéronef léger)
'Pilot-Spacecraft': 'pilot.vaisseau_spatial',
'Pilot-Small Craft': 'pilot.petits_vaisseaux',
'Pilot': 'pilot',
'Flyer': 'pilot.aeronef_atmospherique',
// Astrogation et Navigation (compétences séparées)
'Astrogation': 'astrogation',
'Electronics-Sensors': 'electronics.sensors',
'Navigation': 'navigation',
// Électronique - MgT2e a une compétence "electronics" (confirmé par npcHelper.js:34)
// Révisé : "computers" → "informatique" pour alignement avec le libellé FR
'Electronics-Sensors': 'electronics.capteurs',
'Electronics-Communications': 'electronics.communications',
'Electronics-Computers': 'electronics.computers',
'Electronics-Computers': 'electronics.informatique',
'Electronics': 'electronics',
'Gunner-Turrets': 'gunner.turrets',
'Gunner-Screens': 'gunner.screens',
'Gunner': 'gunner',
'Computers': 'electronics',
'Communications': 'electronics',
// Artillerie - MgT2e utilise "gunner" ou "guncombat" ?
// Dans npcHelper.js:37 on voit "guncombat", donc utilisons ça
'Gunner-Turrets': 'guncombat.tourelles',
'Gunner-Screens': 'guncombat.boucliers',
'Gunner': 'guncombat',
// Mécanique
'Mechanic': 'mechanic',
'Engineer-MDrive': 'engineer.mdrive',
'Engineer-Power': 'engineer.power',
'Engineer-JDrive': 'engineer.jdrive',
'Engineer-Life Support': 'engineer.lifesupport',
// Ingénierie - MgT2e utilise probablement "engineer"
'Engineer-MDrive': 'engineer.propulsion_manoeuvre',
'Engineer-Power': 'engineer.energie',
'Engineer-JDrive': 'engineer.propulsion_saut',
'Engineer-Life Support': 'engineer.support_vie',
'Engineer': 'engineer',
// Social et Administration - tous confirmés dans npcHelper.js
'Steward': 'steward',
'Carouse': 'carouse',
'Persuade': 'persuade',
'Broker': 'broker',
'Admin': 'admin',
'Computers': 'electronics.computers',
'Language': 'language',
'Advocate': 'advocate',
'Leadership': 'leadership',
'Medic': 'medic',
'Streetwise': 'streetwise',
'Diplomat': 'diplomat',
'Science-Biology': 'science.biology',
'Science-Chemistry': 'science.chemistry',
// Sciences
'Science-Biology': 'science.biologie',
'Science-Chemistry': 'science.chimie',
'Science': 'science',
'Deception': 'deception',
'Investigate': 'investigate',
// Combat - "guncombat" confirmé dans npcHelper.js:37
'Gun Combat': 'guncombat',
'Heavy Weapons': 'heavyweapons',
'Melee-Unarmed': 'melee.unarmed',
'Melee-Blade': 'melee.blade',
// Mêlée - "melee" confirmé dans npcHelper.js:37
'Melee-Unarmed': 'melee.sans_arme',
'Melee-Blade': 'melee.arme_blanche',
'Melee': 'melee',
'Athletics-Strength': 'athletics.strength',
'Athletics-Dexterity': 'athletics.dexterity',
// Athlétisme - probablement "athletics"
// Révisé : "dexterite" → "dextérité" (avec accent)
'Athletics-Strength': 'athletics.force',
'Athletics-Dexterity': 'athletics.dextérité',
'Athletics': 'athletics',
// Tactique et Exploration - "tactics" et "recon" confirmés dans npcHelper.js
'Tactics': 'tactics',
'Recon': 'recon',
'Survival': 'survival',
'Navigation': 'navigation',
'Stealth': 'stealth',
'Explosives': 'explosives',
'Communications': 'electronics.communications',
'Drive-Grav': 'drive.grav',
'Deception': 'deception',
'Investigate': 'investigate',
// Conduite - probablement "drive"
// Révisé : "gravite" → "gravité" (avec accent)
'Drive-Grav': 'drive.gravité',
'Drive': 'drive',
// Équipement - probablement "vaccsuit" ou similaire
'Vacc Suit': 'vaccsuit',
'Flyer': 'flyer',
'Art-Acting': 'art.acting',
// Art - probablement "art"
// Révisé : "jeu_d_acteur" → "jeu_acteur" (plus naturel)
'Art-Acting': 'art.jeu_acteur',
'Art-Instrument': 'art.instrument',
'Art': 'art'
};
@@ -114,7 +153,16 @@ const SKILL_MAPPING = {
* @returns {string} - Nom au format mgt2e
*/
function convertSkillToMgt2eFormat(skillName) {
return SKILL_MAPPING[skillName] || skillName.toLowerCase().replace(/-| /g, '.');
// Vérifier d'abord dans le mapping explicite
if (SKILL_MAPPING[skillName]) {
return SKILL_MAPPING[skillName];
}
// Si pas dans le mapping, essayer de deviner
// MgT2e utilise des noms en minuscules (ex: pilot, electronics, guncombat)
// 1. Remplacer les tirets et espaces par des points
// 2. Tout mettre en minuscules
return skillName.toLowerCase().replace(/-| /g, '.');
}
/**