diff --git a/module.json b/module.json
index f48fe53..e79fc40 100644
--- a/module.json
+++ b/module.json
@@ -1,7 +1,7 @@
{
"id": "mgt2-compendium-amiral-denisov",
"title": "MgT2e - Compendium Amiral Denisov",
- "version": "1.2.1",
+ "version": "1.3.0",
"compatibility": {
"minimum": "13",
"verified": "13",
@@ -11,8 +11,13 @@
"esmodules": [
"scripts/commerce.js",
"scripts/npc.js",
+ "scripts/utils/travellerNpcUtils.js",
+ "scripts/data/travellerNpcGenerator.js",
+ "scripts/travellerNpcGenerator.js",
"scripts/TravellerNpcDialog.js",
- "scripts/travellerNpcGenerator.js"
+ "scripts/mgt2eMigration.js",
+ "scripts/npcRollTableSync.js",
+ "scripts/mgt2eSkills.js"
],
"styles": [
"styles/commerce.css",
diff --git a/scripts/TravellerNpcDialog.js b/scripts/TravellerNpcDialog.js
index 2e0c73d..c27e3c1 100644
--- a/scripts/TravellerNpcDialog.js
+++ b/scripts/TravellerNpcDialog.js
@@ -11,8 +11,11 @@ import {
ROLE_LIST,
GENDER_LIST,
DEFAULT_OPTIONS,
- CHARACTERISTIC_LIST,
- UPP_ORDER
+ generateRandomName,
+ CITIZEN_CATEGORY_LABELS_FR,
+ EXPERIENCE_LEVEL_LABELS_FR,
+ ROLE_LABELS_FR,
+ GENDER_LABELS_FR
} from './data/travellerNpcGenerator.js';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@@ -50,11 +53,18 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
gender: options.gender || DEFAULT_OPTIONS.gender,
firstName: options.firstName || '',
surname: options.surname || '',
- useRandomName: options.useRandomName !== false, // Par défaut, on utilise des noms aléatoires
+ useRandomName: options.useRandomName !== false,
createActor: options.createActor !== undefined ? options.createActor : DEFAULT_OPTIONS.createActor,
actorName: options.actorName || '',
openCreatedActor: options.openCreatedActor !== undefined ? options.openCreatedActor : DEFAULT_OPTIONS.openCreatedActor,
};
+
+ // Bind les méthodes pour éviter les problèmes de contexte
+ this._readForm = this._readForm.bind(this);
+ this._handleGenerate = this._handleGenerate.bind(this);
+ this._randomizeName = this._randomizeName.bind(this);
+ this._applyThemeStyles = this._applyThemeStyles.bind(this);
+ this._getForm = this._getForm.bind(this);
}
async _prepareContext() {
@@ -63,22 +73,22 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
...this._formData,
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
key: c.key,
- label: c.label,
+ label: CITIZEN_CATEGORY_LABELS_FR[c.key] || c.label,
description: c.description
})),
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
key: e.key,
- label: e.label,
+ label: EXPERIENCE_LEVEL_LABELS_FR[e.key] || e.label,
description: e.description
})),
roles: ROLE_LIST.map(r => ({
key: r.key,
- label: r.label,
+ label: ROLE_LABELS_FR[r.key] || r.label,
description: r.description
})),
genders: GENDER_LIST.map(g => ({
key: g.key,
- label: g.label
+ label: GENDER_LABELS_FR[g.key] || g.label
}))
};
}
@@ -106,6 +116,7 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
// Gestion du basculement entre nom aléatoire et nom personnalisé
html.find('[name="useRandomName"]').on('change', (event) => {
const useRandom = event.target.checked;
+ this._formData.useRandomName = useRandom;
html.find('.name-fields').toggleClass('hidden', useRandom);
});
@@ -118,31 +129,9 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
}
_applyThemeStyles(html) {
- // Appliquer les styles de thème cohérents avec le dialogue existant
- html.find('.tabs .item').css({
- color: '#d8c79a',
- 'text-shadow': 'none',
- 'background-color': '',
- 'border-bottom-color': 'transparent'
- });
-
- html.find('.tabs .item.active').css({
- color: '#d9b24c',
- 'text-shadow': 'none',
- 'background-color': 'rgba(201, 162, 39, 0.18)',
- 'border-bottom-color': '#c9a227'
- });
-
- html.find('h3').css({
- color: '#5f4300',
- 'border-bottom-color': '#b78f26',
- 'text-shadow': 'none'
- });
-
- html.find('legend').css({
- color: '#7a5c00',
- 'text-shadow': 'none'
- });
+ // Les styles sont maintenant gérés par CSS, cette méthode peut être vide
+ // ou utilisée pour des ajustements spécifiques si nécessaire
+ // Les styles de base sont cohérents avec mgt2-npc-dialog et mgt2-commerce-dialog
}
_readForm(html) {
@@ -159,17 +148,14 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
}
_randomizeName(html) {
- // Importer dynamiquement pour éviter les dépendances circulaires
- import('./data/travellerNpcGenerator.js').then(module => {
- const name = module.generateRandomName(this._formData.gender);
- html.find('[name="firstName"]').val(name.firstName);
- html.find('[name="surname"]').val(name.surname);
- this._formData.firstName = name.firstName;
- this._formData.surname = name.surname;
- this._formData.useRandomName = false;
- html.find('[name="useRandomName"]').prop('checked', false);
- html.find('.name-fields').removeClass('hidden');
- });
+ const name = generateRandomName(this._formData.gender);
+ html.find('[name="firstName"]').val(name.firstName);
+ html.find('[name="surname"]').val(name.surname);
+ this._formData.firstName = name.firstName;
+ this._formData.surname = name.surname;
+ this._formData.useRandomName = false;
+ html.find('[name="useRandomName"]').prop('checked', false);
+ html.find('.name-fields').removeClass('hidden');
}
async _handleGenerate() {
@@ -238,6 +224,9 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
// Helper functions
// ============================================================================
+// Import des données pour les helpers
+import { CHARACTERISTIC_LIST, UPP_ORDER } from './data/travellerNpcGenerator.js';
+
let helpersRegistered = false;
function registerHandlebarsHelpers() {
@@ -279,23 +268,6 @@ function registerHandlebarsHelpers() {
return 'skill-level-0';
});
- // Helper pour formater une compétence avec son niveau
- Handlebars.registerHelper('formatSkillForDisplay', (name, level) => {
- if (level === 0) {
- return name;
- }
- return `${name}-${level}`;
- });
-
- // Helper pour créer un objet de libellés de caractéristiques
- Handlebars.registerHelper('createCharacteristicLabels', () => {
- const labels = {};
- CHARACTERISTIC_LIST.forEach(char => {
- labels[char.key] = char.label;
- });
- return labels;
- });
-
// Helper pour lookup dans un objet
Handlebars.registerHelper('lookup', (obj, key) => {
if (!obj || !key) return '';
diff --git a/scripts/data/travellerNpcGenerator.js b/scripts/data/travellerNpcGenerator.js
index 7713271..54851c0 100644
--- a/scripts/data/travellerNpcGenerator.js
+++ b/scripts/data/travellerNpcGenerator.js
@@ -8,7 +8,191 @@
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
+// Données par défaut
// ============================================================================
+
+export const DEFAULT_OPTIONS = {
+ citizenCategory: CITIZEN_CATEGORY.AVERAGE.key,
+ experience: EXPERIENCE_LEVEL.REGULAR.key,
+ role: ROLE.PILOT.key,
+ gender: GENDER.UNSPECIFIED.key,
+ createActor: false,
+ actorName: '',
+ openCreatedActor: true
+};
+=======
+// ============================================================================
+// Traductions françaises des compétences Traveller
+// ============================================================================
+
+/**
+ * Libellés français des compétences Traveller
+ * Basé sur les traductions du système mgt2e et les standards Traveller FR
+ */
+export const SKILL_LABELS_FR = {
+ // Pilotage
+ 'Pilot-Spacecraft': 'Pilote – Vaisseau spatial',
+ 'Pilot-Small Craft': 'Pilote – Aéronef léger',
+ 'Pilot': 'Pilote',
+ 'Flyer': 'Pilote – Aéronef atmosphérique',
+
+ // Navigation
+ 'Astrogation': 'Astrogation',
+ 'Navigation': 'Navigation',
+
+ // Électronique
+ 'Electronics-Sensors': 'Électronique – Capteurs',
+ 'Electronics-Communications': 'Électronique – Communications',
+ 'Electronics-Computers': 'Électronique – Informatique',
+ 'Electronics': 'Électronique',
+ 'Computers': 'Informatique',
+
+ // Armement
+ 'Gunner-Turrets': 'Artilleur – Tourelles',
+ 'Gunner-Screens': 'Artilleur – Boucliers',
+ 'Gunner': 'Artilleur',
+ 'Gun Combat': 'Combat aux armes à feu',
+ 'Heavy Weapons': 'Armes lourdes',
+ 'Explosives': 'Explosifs',
+
+ // Mécanique et Ingénierie
+ 'Mechanic': 'Mécanique',
+ 'Engineer-MDrive': 'Ingénieur – Propulsion manœuvre',
+ 'Engineer-Power': 'Ingénieur – Énergie',
+ 'Engineer-JDrive': 'Ingénieur – Propulsion saut',
+ 'Engineer-Life Support': 'Ingénieur – Support vie',
+ 'Engineer': 'Ingénieur',
+
+ // Social et Administration
+ 'Steward': 'Intendant',
+ 'Carouse': 'Festoyer',
+ 'Persuade': 'Persuasion',
+ 'Broker': 'Courtage',
+ 'Admin': 'Administration',
+ 'Advocate': 'Plaidoyer',
+ 'Diplomat': 'Diplomatie',
+ 'Streetwise': 'Rues',
+ 'Leadership': 'Direction',
+
+ // Sciences
+ 'Science-Biology': 'Science – Biologie',
+ 'Science-Chemistry': 'Science – Chimie',
+ 'Science': 'Science',
+
+ // Santé
+ 'Medic': 'Médecine',
+
+ // Investigation
+ 'Deception': 'Tromperie',
+ 'Investigate': 'Investigation',
+
+ // Combat
+ 'Melee-Unarmed': 'Mêlée – Sans arme',
+ 'Melee-Blade': 'Mêlée – Arme blanche',
+ 'Melee': 'Mêlée',
+ 'Athletics-Strength': 'Athlétisme – Force',
+ 'Athletics-Dexterity': 'Athlétisme – Dextérité',
+ 'Athletics': 'Athlétisme',
+
+ // Tactiques
+ 'Tactics': 'Tactiques',
+
+ // Reconnaissance et Survie
+ 'Recon': 'Reconnaissance',
+ 'Survival': 'Survie',
+ 'Stealth': 'Discrétion',
+
+ // Communications
+ 'Communications': 'Communications',
+
+ // Conduite
+ 'Drive-Grav': 'Conduite – Gravité',
+ 'Drive': 'Conduite',
+
+ // Autres
+ 'Vacc Suit': 'Combinaison spatiale',
+ 'Language': 'Langue',
+ 'Art-Acting': 'Art – Jeu d\'acteur',
+ 'Art-Instrument': 'Art – Instrument',
+ 'Art': 'Art'
+};
+
+/**
+ * Libellés français des caractéristiques
+ */
+export const CHARACTERISTIC_LABELS_FR = {
+ 'STR': 'Force',
+ 'DEX': 'Dextérité',
+ 'END': 'Endurance',
+ 'INT': 'Intellect',
+ 'EDU': 'Éducation',
+ 'SOC': 'Statut Social'
+};
+
+/**
+ * Libellés français des catégories de citoyen
+ */
+export const CITIZEN_CATEGORY_LABELS_FR = {
+ 'belowAverage': 'En dessous de la moyenne',
+ 'average': 'Moyenne',
+ 'aboveAverage': 'Au-dessus de la moyenne',
+ 'exceptional': 'Exceptionnel'
+};
+
+/**
+ * Libellés français des niveaux d'expérience
+ */
+export const EXPERIENCE_LEVEL_LABELS_FR = {
+ 'recruit': 'Recrue',
+ 'rookie': 'Débutant',
+ 'intermediate': 'Intermédiaire',
+ 'regular': 'Régulier',
+ 'veteran': 'Vétéran',
+ 'elite': 'Élite'
+};
+
+/**
+ * Libellés français des rôles
+ */
+export const ROLE_LABELS_FR = {
+ 'pilot': 'Pilote',
+ 'navigator': 'Navigateur',
+ 'engineer': 'Ingénieur',
+ 'steward': 'Intendant',
+ 'medic': 'Médecin',
+ 'marine': 'Marine',
+ 'gunner': 'Artilleur',
+ 'scout': 'Éclaireur',
+ 'technician': 'Technicien',
+ 'leader': 'Chef',
+ 'diplomat': 'Diplomate',
+ 'entertainer': 'Artiste',
+ 'trader': 'Marchand',
+ 'thug': 'Brute'
+};
+
+/**
+ * Libellés français des genres
+ */
+export const GENDER_LABELS_FR = {
+ 'unspecified': 'Non spécifié',
+ 'female': 'Féminin',
+ 'male': 'Masculin'
+};
+
+// ============================================================================
+// Données par défaut
+// ============================================================================
+
+export const DEFAULT_OPTIONS = {
+ citizenCategory: CITIZEN_CATEGORY.AVERAGE.key,
+ experience: EXPERIENCE_LEVEL.REGULAR.key,
+ role: ROLE.PILOT.key,
+ gender: GENDER.UNSPECIFIED.key,
+ createActor: false,
+ actorName: '',
+ openCreatedActor: true
+};============================================================================
// Catégories de citoyens
// ============================================================================
@@ -420,7 +604,6 @@ export const ROLE_SKILLS = {
'Streetwise',
'Carouse',
'Tactics',
- 'Stealth',
'Survival',
'Persuade',
'Explosives',
@@ -547,6 +730,21 @@ export const CHARACTERISTIC_LIST = [
// Ordre des caractéristiques pour l'UPP
export const UPP_ORDER = ['STR', 'DEX', 'END', 'INT', 'EDU', 'SOC'];
+// ============================================================================
+// Codes d'erreur
+// ============================================================================
+
+export const ERROR_CODES = {
+ INVALID_OPTIONS: 'INVALID_OPTIONS',
+ INVALID_ROLE: 'INVALID_ROLE',
+ INVALID_CATEGORY: 'INVALID_CATEGORY',
+ INVALID_EXPERIENCE: 'INVALID_EXPERIENCE',
+ INVALID_GENDER: 'INVALID_GENDER',
+ ACTOR_CREATION_FAILED: 'ACTOR_CREATION_FAILED',
+ MGT2E_NOT_ACTIVE: 'MGT2E_NOT_ACTIVE',
+ BASE_ACTOR_NOT_FOUND: 'BASE_ACTOR_NOT_FOUND'
+};
+
// ============================================================================
// Fonctions utilitaires
// ============================================================================
@@ -645,7 +843,7 @@ export function popRandomItems(array, count) {
/**
* Trouve un rôle par sa clé
* @param {string} key - Clé du rôle
- * @returns {Object|null} - Objet rôle ou null
+ * @returns {Object} - Objet rôle
*/
export function getRoleByKey(key) {
const found = ROLE_LIST.find(r => r.key === key);
@@ -700,6 +898,42 @@ export function getCharacteristicPrioritiesForRole(roleKey) {
return CHARACTERISTIC_PRIORITIES[roleKey] || CHARACTERISTIC_PRIORITIES.pilot;
}
+/**
+ * Valide les options de génération
+ * @param {Object} options - Options à valider
+ * @returns {Object} - Options validées
+ */
+export function validateOptions(options = {}) {
+ const errors = [];
+ const validated = { ...options };
+
+ if (validated.citizenCategory && !getCitizenCategoryByKey(validated.citizenCategory)) {
+ errors.push(`Catégorie de citoyen invalide: ${validated.citizenCategory}`);
+ validated.citizenCategory = DEFAULT_OPTIONS.citizenCategory;
+ }
+
+ if (validated.experience && !getExperienceLevelByKey(validated.experience)) {
+ errors.push(`Niveau d'expérience invalide: ${validated.experience}`);
+ validated.experience = DEFAULT_OPTIONS.experience;
+ }
+
+ if (validated.role && !getRoleByKey(validated.role)) {
+ errors.push(`Rôle invalide: ${validated.role}`);
+ validated.role = DEFAULT_OPTIONS.role;
+ }
+
+ if (validated.gender && !getGenderByKey(validated.gender)) {
+ errors.push(`Genre invalide: ${validated.gender}`);
+ validated.gender = DEFAULT_OPTIONS.gender;
+ }
+
+ if (errors.length > 0) {
+ console.warn(`${MODULE_ID} | Options de génération invalides:`, errors);
+ }
+
+ return validated;
+}
+
// ============================================================================
// Données par défaut
// ============================================================================
diff --git a/scripts/tests/travellerNpcGenerator.test.js b/scripts/tests/travellerNpcGenerator.test.js
new file mode 100644
index 0000000..163d3d6
--- /dev/null
+++ b/scripts/tests/travellerNpcGenerator.test.js
@@ -0,0 +1,1096 @@
+/**
+ * Tests unitaires pour le générateur de PNJ Traveller
+ *
+ * Ces tests vérifient le bon fonctionnement des fonctions de génération
+ * sans nécessiter l'environnement Foundry VTT.
+ *
+ * Pour exécuter ces tests, utilisez un environnement Node.js avec Jest ou
+ * un framework de test similaire.
+ */
+
+// Mock des dépendances Foundry pour l'environnement de test
+const mockFoundry = {
+ utils: {
+ deepClone: (obj) => JSON.parse(JSON.stringify(obj)),
+ mergeObject: (target, source) => ({ ...target, ...source }),
+ setProperty: () => {},
+ getProperty: () => {}
+ },
+ applications: {
+ handlebars: {
+ renderTemplate: async () => '
Test
'
+ }
+ }
+};
+
+global.foundry = mockFoundry;
+
+// Mock console pour capturer les logs
+const mockConsole = {
+ log: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn()
+};
+
+// Remplacer la console globale
+console.log = mockConsole.log;
+console.warn = mockConsole.warn;
+console.error = mockConsole.error;
+console.debug = mockConsole.debug;
+
+// Mock ui.notifications
+const mockUi = {
+ notifications: {
+ info: jest.fn(),
+ error: jest.fn(),
+ warn: jest.fn(),
+ success: jest.fn()
+ }
+};
+
+global.ui = mockUi;
+
+// Mock game object
+const mockGame = {
+ system: { id: 'mgt2e' },
+ packs: {
+ get: jest.fn()
+ },
+ i18n: {
+ localize: jest.fn((key) => key)
+ },
+ settings: {
+ get: jest.fn()
+ }
+};
+
+global.game = mockGame;
+
+// Mock canvas
+const mockCanvas = {
+ tokens: {
+ controlled: []
+ }
+};
+
+global.canvas = mockCanvas;
+
+// Imports des modules à tester
+import {
+ generateCharacteristics,
+ generateSkills,
+ generateTravellerNpc,
+ generateAndCreateTravellerNpc,
+ convertSkillToMgt2eFormat,
+ buildMgt2eCharacteristics,
+ buildMgt2eSkills,
+ buildMgt2eBaseActorSystem,
+ getMgt2eBaseActorSystem,
+ toHex,
+ calculateDm,
+ pickRandomItem,
+ shuffleArray,
+ popRandomItems,
+ getRoleByKey,
+ getCitizenCategoryByKey,
+ getExperienceLevelByKey,
+ getGenderByKey,
+ getSkillsForRole,
+ getCharacteristicPrioritiesForRole,
+ validateOptions,
+ DEFAULT_OPTIONS,
+ CITIZEN_CATEGORY_LIST,
+ EXPERIENCE_LEVEL_LIST,
+ ROLE_LIST,
+ GENDER_LIST,
+ CHARACTERISTIC_LIST,
+ UPP_ORDER,
+ ERROR_CODES,
+ TravellerNpcError
+} from '../travellerNpcGenerator.js';
+
+import {
+ CITIZEN_CATEGORY,
+ EXPERIENCE_LEVEL,
+ ROLE,
+ GENDER,
+ CHARACTERISTIC,
+ CHARACTERISTIC_PRIORITIES,
+ ROLE_SKILLS,
+ NAME_CATALOGS
+} from '../data/travellerNpcGenerator.js';
+
+import {
+ ModuleCache,
+ travellerNpcCache,
+ ERROR_CODES as UTILS_ERROR_CODES
+} from '../utils/travellerNpcUtils.js';
+
+// ============================================================================
+// Tests des fonctions utilitaires
+// ============================================================================
+
+describe('Fonctions utilitaires', () => {
+ describe('toHex', () => {
+ it('devrait convertir 0 en "0"', () => {
+ expect(toHex(0)).toBe('0');
+ });
+
+ it('devrait convertir 7 en "7"', () => {
+ expect(toHex(7)).toBe('7');
+ });
+
+ it('devrait convertir 10 en "A"', () => {
+ expect(toHex(10)).toBe('A');
+ });
+
+ it('devrait convertir 15 en "F"', () => {
+ expect(toHex(15)).toBe('F');
+ });
+
+ it('devrait limiter les valeurs à 0-15', () => {
+ expect(toHex(-5)).toBe('0');
+ expect(toHex(20)).toBe('F');
+ });
+
+ it('devrait arrondir vers le bas', () => {
+ expect(toHex(10.7)).toBe('A');
+ expect(toHex(10.2)).toBe('A');
+ });
+ });
+
+ describe('calculateDm', () => {
+ it('devrait calculer le DM pour la valeur 6 (DM=0)', () => {
+ expect(calculateDm(6)).toBe(0);
+ });
+
+ it('devrait calculer le DM pour la valeur 7 (DM=0)', () => {
+ expect(calculateDm(7)).toBe(0);
+ });
+
+ it('devrait calculer le DM pour la valeur 8 (DM=0)', () => {
+ expect(calculateDm(8)).toBe(0);
+ });
+
+ it('devrait calculer le DM pour la valeur 9 (DM=+1)', () => {
+ expect(calculateDm(9)).toBe(1);
+ });
+
+ it('devrait calculer le DM pour la valeur 12 (DM=+2)', () => {
+ expect(calculateDm(12)).toBe(2);
+ });
+
+ it('devrait calculer le DM pour la valeur 4 (DM=-1)', () => {
+ expect(calculateDm(4)).toBe(-1);
+ });
+
+ it('devrait calculer le DM pour la valeur 3 (DM=-1)', () => {
+ expect(calculateDm(3)).toBe(-1);
+ });
+
+ it('devrait calculer le DM pour la valeur 1 (DM=-2)', () => {
+ expect(calculateDm(1)).toBe(-2);
+ });
+ });
+
+ describe('pickRandomItem', () => {
+ it('devrait retourner un élément aléatoire du tableau', () => {
+ const items = [1, 2, 3, 4, 5];
+ const result = pickRandomItem(items);
+ expect(items).toContain(result);
+ });
+
+ it('devrait lancer une erreur pour un tableau vide', () => {
+ expect(() => pickRandomItem([])).toThrow('Cannot pick from empty array');
+ });
+
+ it('devrait lancer une erreur pour null/undefined', () => {
+ expect(() => pickRandomItem(null)).toThrow('Cannot pick from empty array');
+ expect(() => pickRandomItem(undefined)).toThrow('Cannot pick from empty array');
+ });
+ });
+
+ describe('shuffleArray', () => {
+ it('devrait retourner un tableau de même longueur', () => {
+ const array = [1, 2, 3, 4, 5];
+ const shuffled = shuffleArray(array);
+ expect(shuffled).toHaveLength(5);
+ });
+
+ it('devrait retourner tous les éléments originaux', () => {
+ const array = [1, 2, 3, 4, 5];
+ const shuffled = shuffleArray(array);
+ expect(shuffled).toEqual(expect.arrayContaining(array));
+ });
+
+ it('devrait retourner un tableau différent (sauf cas très rare)', () => {
+ const array = [1, 2, 3, 4, 5];
+ const shuffled = shuffleArray(array);
+ // Note: Il y a une très faible probabilité que le tableau reste identique
+ // Cette assertion peut échouer très rarement
+ expect(shuffled).not.toEqual(array);
+ });
+
+ it('devrait retourner une copie (ne pas modifier l\'original)', () => {
+ const array = [1, 2, 3, 4, 5];
+ const original = [...array];
+ shuffleArray(array);
+ expect(array).toEqual(original);
+ });
+ });
+
+ describe('popRandomItems', () => {
+ it('devrait retourner le bon nombre d\'éléments extraits', () => {
+ const array = [1, 2, 3, 4, 5, 6, 7, 8];
+ const [taken, remaining] = popRandomItems(array, 3);
+ expect(taken).toHaveLength(3);
+ });
+
+ it('devrait retourner les éléments restants', () => {
+ const array = [1, 2, 3, 4, 5, 6, 7, 8];
+ const [taken, remaining] = popRandomItems(array, 3);
+ expect(remaining).toHaveLength(5);
+ });
+
+ it('devrait retourner tous les éléments si count > array.length', () => {
+ const array = [1, 2, 3];
+ const [taken, remaining] = popRandomItems(array, 10);
+ expect(taken).toHaveLength(3);
+ expect(remaining).toHaveLength(0);
+ });
+
+ it('devrait retourner un tableau vide si count <= 0', () => {
+ const array = [1, 2, 3, 4, 5];
+ const [taken, remaining] = popRandomItems(array, 0);
+ expect(taken).toHaveLength(0);
+ expect(remaining).toHaveLength(5);
+ });
+
+ it('devrait retourner des tableaux vides pour un tableau vide', () => {
+ const [taken, remaining] = popRandomItems([], 3);
+ expect(taken).toHaveLength(0);
+ expect(remaining).toHaveLength(0);
+ });
+ });
+});
+
+// ============================================================================
+// Tests des fonctions de lookup
+// ============================================================================
+
+describe('Fonctions de lookup', () => {
+ describe('getRoleByKey', () => {
+ it('devrait retourner le rôle pilote', () => {
+ const role = getRoleByKey('pilot');
+ expect(role.key).toBe('pilot');
+ });
+
+ it('devrait retourner le rôle par défaut (pilot) pour une clé invalide', () => {
+ const role = getRoleByKey('invalid');
+ expect(role.key).toBe('pilot');
+ });
+
+ it('devrait retourner le rôle navigateur', () => {
+ const role = getRoleByKey('navigator');
+ expect(role.key).toBe('navigator');
+ });
+ });
+
+ describe('getCitizenCategoryByKey', () => {
+ it('devrait retourner la catégorie moyenne', () => {
+ const category = getCitizenCategoryByKey('average');
+ expect(category.key).toBe('average');
+ });
+
+ it('devrait retourner la catégorie par défaut (average) pour une clé invalide', () => {
+ const category = getCitizenCategoryByKey('invalid');
+ expect(category.key).toBe('average');
+ });
+
+ it('devrait retourner la catégorie exceptionnel', () => {
+ const category = getCitizenCategoryByKey('exceptional');
+ expect(category.key).toBe('exceptional');
+ });
+ });
+
+ describe('getExperienceLevelByKey', () => {
+ it('devrait retourner le niveau recruit', () => {
+ const level = getExperienceLevelByKey('recruit');
+ expect(level.key).toBe('recruit');
+ });
+
+ it('devrait retourner le niveau par défaut (regular) pour une clé invalide', () => {
+ const level = getExperienceLevelByKey('invalid');
+ expect(level.key).toBe('regular');
+ });
+
+ it('devrait retourner le niveau elite', () => {
+ const level = getExperienceLevelByKey('elite');
+ expect(level.key).toBe('elite');
+ });
+ });
+
+ describe('getGenderByKey', () => {
+ it('devrait retourner le genre unspecified', () => {
+ const gender = getGenderByKey('unspecified');
+ expect(gender.key).toBe('unspecified');
+ });
+
+ it('devrait retourner le genre par défaut (unspecified) pour une clé invalide', () => {
+ const gender = getGenderByKey('invalid');
+ expect(gender.key).toBe('unspecified');
+ });
+
+ it('devrait retourner le genre female', () => {
+ const gender = getGenderByKey('female');
+ expect(gender.key).toBe('female');
+ });
+ });
+
+ describe('getSkillsForRole', () => {
+ it('devrait retourner les compétences pour pilote', () => {
+ const skills = getSkillsForRole('pilot');
+ expect(skills).toBeDefined();
+ expect(skills.length).toBeGreaterThan(0);
+ expect(skills).toContain('Pilot-Spacecraft');
+ });
+
+ it('devrait retourner les compétences pour médecin', () => {
+ const skills = getSkillsForRole('medic');
+ expect(skills).toContain('Medic');
+ });
+
+ it('devrait retourner les compétences par défaut pour un rôle invalide', () => {
+ const skills = getSkillsForRole('invalid');
+ expect(skills).toBeDefined();
+ expect(skills.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('getCharacteristicPrioritiesForRole', () => {
+ it('devrait retourner les priorités pour pilote', () => {
+ const priorities = getCharacteristicPrioritiesForRole('pilot');
+ expect(priorities).toBeDefined();
+ expect(priorities.high).toContain('DEX');
+ expect(priorities.high).toContain('INT');
+ });
+
+ it('devrait retourner les priorités par défaut pour un rôle invalide', () => {
+ const priorities = getCharacteristicPrioritiesForRole('invalid');
+ expect(priorities).toBeDefined();
+ });
+ });
+});
+
+// ============================================================================
+// Tests de validation des options
+// ============================================================================
+
+describe('validateOptions', () => {
+ it('devrait retourner les options par défaut pour un objet vide', () => {
+ const validated = validateOptions({});
+ expect(validated.citizenCategory).toBe(DEFAULT_OPTIONS.citizenCategory);
+ expect(validated.experience).toBe(DEFAULT_OPTIONS.experience);
+ expect(validated.role).toBe(DEFAULT_OPTIONS.role);
+ expect(validated.gender).toBe(DEFAULT_OPTIONS.gender);
+ });
+
+ it('devrait accepter des options valides', () => {
+ const validated = validateOptions({
+ citizenCategory: 'exceptional',
+ experience: 'elite',
+ role: 'engineer',
+ gender: 'female'
+ });
+ expect(validated.citizenCategory).toBe('exceptional');
+ expect(validated.experience).toBe('elite');
+ expect(validated.role).toBe('engineer');
+ expect(validated.gender).toBe('female');
+ });
+
+ it('devrait corriger une catégorie de citoyen invalide', () => {
+ const validated = validateOptions({
+ citizenCategory: 'invalid',
+ experience: 'regular',
+ role: 'pilot',
+ gender: 'male'
+ });
+ expect(validated.citizenCategory).toBe(DEFAULT_OPTIONS.citizenCategory);
+ });
+
+ it('devrait corriger un niveau d\'expérience invalide', () => {
+ const validated = validateOptions({
+ citizenCategory: 'average',
+ experience: 'invalid',
+ role: 'pilot',
+ gender: 'male'
+ });
+ expect(validated.experience).toBe(DEFAULT_OPTIONS.experience);
+ });
+
+ it('devrait corriger un rôle invalide', () => {
+ const validated = validateOptions({
+ citizenCategory: 'average',
+ experience: 'regular',
+ role: 'invalid',
+ gender: 'male'
+ });
+ expect(validated.role).toBe(DEFAULT_OPTIONS.role);
+ });
+
+ it('devrait corriger un genre invalide', () => {
+ const validated = validateOptions({
+ citizenCategory: 'average',
+ experience: 'regular',
+ role: 'pilot',
+ gender: 'invalid'
+ });
+ expect(validated.gender).toBe(DEFAULT_OPTIONS.gender);
+ });
+
+ it('devrait conserver les options personnalisées valides', () => {
+ const validated = validateOptions({
+ citizenCategory: 'aboveAverage',
+ experience: 'veteran',
+ role: 'gunner',
+ gender: 'male',
+ createActor: true,
+ actorName: 'Test NPC',
+ openCreatedActor: false
+ });
+ expect(validated.createActor).toBe(true);
+ expect(validated.actorName).toBe('Test NPC');
+ expect(validated.openCreatedActor).toBe(false);
+ });
+});
+
+// ============================================================================
+// Tests de génération des caractéristiques
+// ============================================================================
+
+describe('generateCharacteristics', () => {
+ it('devrait générer des caractéristiques pour la catégorie average', () => {
+ const result = generateCharacteristics('average', 'pilot');
+ expect(result.characteristics).toBeDefined();
+ expect(result.upp).toBeDefined();
+ expect(result.category).toBeDefined();
+ expect(result.category.key).toBe('average');
+ });
+
+ it('devrait générer des caractéristiques pour la catégorie exceptional', () => {
+ const result = generateCharacteristics('exceptional', 'pilot');
+ expect(result.category.key).toBe('exceptional');
+ expect(result.characteristics.STR).toBeGreaterThanOrEqual(7);
+ expect(result.characteristics.DEX).toBeGreaterThanOrEqual(7);
+ expect(result.characteristics.END).toBeGreaterThanOrEqual(7);
+ });
+
+ it('devrait générer des caractéristiques pour la catégorie belowAverage', () => {
+ const result = generateCharacteristics('belowAverage', 'pilot');
+ expect(result.category.key).toBe('belowAverage');
+ // Les valeurs devraient être plus basses
+ const values = Object.values(result.characteristics);
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
+ expect(avg).toBeLessThan(7);
+ });
+
+ it('devrait générer un code UPP de 6 caractères', () => {
+ const result = generateCharacteristics('average', 'pilot');
+ expect(result.upp).toHaveLength(6);
+ expect(result.upp).toMatch(/^[0-9A-F]{6}$/);
+ });
+
+ it('devrait générer toutes les caractéristiques (STR, DEX, END, INT, EDU, SOC)', () => {
+ const result = generateCharacteristics('average', 'pilot');
+ expect(result.characteristics.STR).toBeDefined();
+ expect(result.characteristics.DEX).toBeDefined();
+ expect(result.characteristics.END).toBeDefined();
+ expect(result.characteristics.INT).toBeDefined();
+ expect(result.characteristics.EDU).toBeDefined();
+ expect(result.characteristics.SOC).toBeDefined();
+ });
+
+ it('devrait attribuer des valeurs plus élevées aux caractéristiques prioritaires', () => {
+ // Pour le pilote, DEX et INT sont prioritaires (high)
+ const priorities = getCharacteristicPrioritiesForRole('pilot');
+ const result = generateCharacteristics('average', 'pilot');
+
+ // DEX et INT devraient avoir des valeurs plus élevées que SOC et END
+ const dexValue = result.characteristics.DEX;
+ const intValue = result.characteristics.INT;
+ const socValue = result.characteristics.SOC;
+ const endValue = result.characteristics.END;
+
+ // Note: Cela peut échouer occasionnellement à cause de l'aléatoire
+ // mais devrait passer dans la plupart des cas
+ expect(dexValue + intValue).toBeGreaterThanOrEqual(socValue + endValue);
+ });
+
+ it('devrait générer des caractéristiques même avec une catégorie invalide', () => {
+ const result = generateCharacteristics('invalid', 'pilot');
+ expect(result.characteristics).toBeDefined();
+ // Devrait utiliser la catégorie par défaut (average)
+ expect(result.category.key).toBe('average');
+ });
+
+ it('devrait générer des caractéristiques même avec un rôle invalide', () => {
+ const result = generateCharacteristics('average', 'invalid');
+ expect(result.characteristics).toBeDefined();
+ });
+});
+
+// ============================================================================
+// Tests de génération des compétences
+// ============================================================================
+
+describe('generateSkills', () => {
+ it('devrait générer des compétences pour un rôle et un niveau', () => {
+ const skills = generateSkills('pilot', 'regular');
+ expect(skills).toBeDefined();
+ expect(Array.isArray(skills)).toBe(true);
+ expect(skills.length).toBeGreaterThan(0);
+ });
+
+ it('devrait générer des compétences avec des niveaux', () => {
+ const skills = generateSkills('pilot', 'regular');
+ skills.forEach(skill => {
+ expect(skill.name).toBeDefined();
+ expect(skill.level).toBeDefined();
+ expect(typeof skill.level).toBe('number');
+ });
+ });
+
+ it('devrait trier les compétences par niveau décroissant', () => {
+ const skills = generateSkills('pilot', 'regular');
+ for (let i = 0; i < skills.length - 1; i++) {
+ expect(skills[i].level).toBeGreaterThanOrEqual(skills[i + 1].level);
+ }
+ });
+
+ it('devrait générer plus de compétences de niveau élevé pour elite', () => {
+ const eliteSkills = generateSkills('pilot', 'elite');
+ const regularSkills = generateSkills('pilot', 'regular');
+
+ const eliteLevel3 = eliteSkills.filter(s => s.level === 3).length;
+ const regularLevel3 = regularSkills.filter(s => s.level === 3).length;
+
+ // Elite devrait avoir plus de compétences de niveau 3
+ expect(eliteLevel3).toBeGreaterThanOrEqual(regularLevel3);
+ });
+
+ it('devrait supprimer les doublons basés sur la compétence de base', () => {
+ const skills = generateSkills('pilot', 'regular');
+ const skillNames = skills.map(s => s.name);
+ const uniqueBases = new Set(skillNames.map(n => n.split('-')[0]));
+
+ // Chaque nom de base devrait apparaître une seule fois
+ expect(skillNames.length).toBe(uniqueBases.size);
+ });
+
+ it('devrait générer des compétences avec le rôle par défaut pour un rôle invalide', () => {
+ const skills = generateSkills('invalid', 'regular');
+ expect(skills).toBeDefined();
+ expect(skills.length).toBeGreaterThan(0);
+ });
+
+ it('devrait générer des compétences avec le niveau par défaut pour un niveau invalide', () => {
+ const skills = generateSkills('pilot', 'invalid');
+ expect(skills).toBeDefined();
+ expect(skills.length).toBeGreaterThan(0);
+ });
+});
+
+// ============================================================================
+// Tests de conversion de compétences
+// ============================================================================
+
+describe('convertSkillToMgt2eFormat', () => {
+ it('devrait convertir Pilot-Spacecraft', () => {
+ expect(convertSkillToMgt2eFormat('Pilot-Spacecraft')).toBe('pilot.spacecraft');
+ });
+
+ it('devrait convertir Gun Combat', () => {
+ expect(convertSkillToMgt2eFormat('Gun Combat')).toBe('guncombat');
+ });
+
+ it('devrait convertir une compétence inconnue', () => {
+ const result = convertSkillToMgt2eFormat('Unknown-Skill');
+ expect(result).toBe('unknown.skill');
+ });
+
+ it('devrait gérer les espaces', () => {
+ expect(convertSkillToMgt2eFormat('Gun Combat')).toBe('guncombat');
+ });
+
+ it('devrait gérer les tirets', () => {
+ expect(convertSkillToMgt2eFormat('Pilot-Small Craft')).toBe('pilot.smallcraft');
+ });
+});
+
+// ============================================================================
+// Tests de génération complète du PNJ
+// ============================================================================
+
+describe('generateTravellerNpc', () => {
+ it('devrait générer un PNJ avec des options par défaut', () => {
+ const npc = generateTravellerNpc();
+ expect(npc.success).toBe(true);
+ expect(npc.type).toBe('traveller-npc');
+ expect(npc.name).toBeDefined();
+ expect(npc.name.firstName).toBeDefined();
+ expect(npc.name.surname).toBeDefined();
+ expect(npc.name.fullName).toBeDefined();
+ expect(npc.role).toBeDefined();
+ expect(npc.citizenCategory).toBeDefined();
+ expect(npc.experience).toBeDefined();
+ expect(npc.gender).toBeDefined();
+ expect(npc.characteristics).toBeDefined();
+ expect(npc.upp).toBeDefined();
+ expect(npc.skills).toBeDefined();
+ expect(npc.skillsForActor).toBeDefined();
+ expect(npc.display).toBeDefined();
+ });
+
+ it('devrait générer un PNJ avec des options personnalisées', () => {
+ const npc = generateTravellerNpc({
+ citizenCategory: 'exceptional',
+ experience: 'elite',
+ role: 'engineer',
+ gender: 'female',
+ firstName: 'Jane',
+ surname: 'Doe'
+ });
+
+ expect(npc.name.firstName).toBe('Jane');
+ expect(npc.name.surname).toBe('Doe');
+ expect(npc.name.fullName).toBe('Jane Doe');
+ expect(npc.role.key).toBe('engineer');
+ expect(npc.citizenCategory.key).toBe('exceptional');
+ expect(npc.experience.key).toBe('elite');
+ expect(npc.gender.key).toBe('female');
+ });
+
+ it('devrait générer un nom aléatoire si aucun nom n\'est fourni', () => {
+ const npc = generateTravellerNpc({
+ gender: 'male'
+ });
+
+ expect(npc.name.firstName).toBeDefined();
+ expect(npc.name.surname).toBeDefined();
+ expect(npc.name.fullName).toContain(' ');
+ expect(NAME_CATALOGS.maleNames).toContain(npc.name.firstName);
+ expect(NAME_CATALOGS.surnames).toContain(npc.name.surname);
+ });
+
+ it('devrait générer un nom féminin', () => {
+ const npc = generateTravellerNpc({
+ gender: 'female'
+ });
+
+ expect(NAME_CATALOGS.femaleNames).toContain(npc.name.firstName);
+ });
+
+ it('devrait générer un nom non-genré', () => {
+ const npc = generateTravellerNpc({
+ gender: 'unspecified'
+ });
+
+ expect(NAME_CATALOGS.nonGenderedNames).toContain(npc.name.firstName);
+ });
+
+ it('devrait inclure des informations d\'affichage', () => {
+ const npc = generateTravellerNpc({
+ role: 'pilot',
+ citizenCategory: 'average',
+ experience: 'regular',
+ gender: 'male'
+ });
+
+ expect(npc.display.roleLabel).toBe('Pilote');
+ expect(npc.display.categoryLabel).toBe('Moyenne');
+ expect(npc.display.experienceLabel).toBe('Régulier');
+ expect(npc.display.genderLabel).toBe('Masculin');
+ expect(npc.display.characteristicLabels).toBeDefined();
+ });
+
+ it('devrait avoir un UPP valide', () => {
+ const npc = generateTravellerNpc();
+ expect(npc.upp).toMatch(/^[0-9A-F]{6}$/);
+ });
+
+ it('devrait avoir des compétences au format mgt2e', () => {
+ const npc = generateTravellerNpc();
+ expect(npc.skillsForActor).toBeDefined();
+ expect(Array.isArray(npc.skillsForActor)).toBe(true);
+ npc.skillsForActor.forEach(skill => {
+ expect(skill.name).toBeDefined();
+ expect(skill.level).toBeDefined();
+ });
+ });
+});
+
+// ============================================================================
+// Tests du ModuleCache
+// ============================================================================
+
+describe('ModuleCache', () => {
+ let cache;
+
+ beforeEach(() => {
+ cache = new ModuleCache('test-module', 1000);
+ });
+
+ describe('getOrFetch', () => {
+ it('devrait fetch et cacher une valeur', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('test-value');
+ const result = await cache.getOrFetch('key1', fetchFn);
+
+ expect(result).toBe('test-value');
+ expect(fetchFn).toHaveBeenCalledTimes(1);
+ });
+
+ it('devrait retourner la valeur du cache sans refetch', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('test-value');
+
+ await cache.getOrFetch('key1', fetchFn);
+ const result = await cache.getOrFetch('key1', fetchFn);
+
+ expect(result).toBe('test-value');
+ expect(fetchFn).toHaveBeenCalledTimes(1); // Pas appelé une deuxième fois
+ });
+
+ it('devrait gérer les erreurs de fetch', async () => {
+ const error = new Error('Fetch error');
+ const fetchFn = jest.fn().mockRejectedValue(error);
+
+ await expect(cache.getOrFetch('key1', fetchFn)).rejects.toThrow('Fetch error');
+ expect(fetchFn).toHaveBeenCalledTimes(1);
+ });
+
+ it('devrait forcer le rafraîchissement avec forceRefresh', async () => {
+ const fetchFn = jest.fn()
+ .mockResolvedValueOnce('value1')
+ .mockResolvedValueOnce('value2');
+
+ await cache.getOrFetch('key1', fetchFn);
+ const result = await cache.getOrFetch('key1', fetchFn, { forceRefresh: true });
+
+ expect(result).toBe('value2');
+ expect(fetchFn).toHaveBeenCalledTimes(2);
+ });
+
+ it('devrait cloner profondément les résultats', async () => {
+ const original = { nested: { value: 1 } };
+ const fetchFn = jest.fn().mockResolvedValue(original);
+
+ const result1 = await cache.getOrFetch('key1', fetchFn);
+ const result2 = await cache.getOrFetch('key1', fetchFn);
+
+ result1.nested.value = 2;
+
+ expect(result2.nested.value).toBe(1);
+ expect(original.nested.value).toBe(1);
+ });
+ });
+
+ describe('clear', () => {
+ it('devrait supprimer une entrée spécifique', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('test-value');
+
+ await cache.getOrFetch('key1', fetchFn);
+ cache.clear('key1');
+
+ await cache.getOrFetch('key1', fetchFn);
+ expect(fetchFn).toHaveBeenCalledTimes(2); // Fetch à nouveau car cache vidé
+ });
+ });
+
+ describe('clearAll', () => {
+ it('devrait vider tout le cache', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('test-value');
+
+ await cache.getOrFetch('key1', fetchFn);
+ await cache.getOrFetch('key2', fetchFn);
+ cache.clearAll();
+
+ await cache.getOrFetch('key1', fetchFn);
+ await cache.getOrFetch('key2', fetchFn);
+ expect(fetchFn).toHaveBeenCalledTimes(4); // Fetch à nouveau pour les deux clés
+ });
+ });
+
+ describe('has', () => {
+ it('devrait retourner true pour une clé en cache', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('test-value');
+
+ await cache.getOrFetch('key1', fetchFn);
+ expect(cache.has('key1')).toBe(true);
+ });
+
+ it('devrait retourner false pour une clé non en cache', () => {
+ expect(cache.has('nonexistent')).toBe(false);
+ });
+ });
+
+ describe('get', () => {
+ it('devrait retourner la valeur du cache', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('test-value');
+
+ await cache.getOrFetch('key1', fetchFn);
+ const result = cache.get('key1');
+
+ expect(result).toBe('test-value');
+ });
+
+ it('devrait retourner null pour une clé non en cache', () => {
+ expect(cache.get('nonexistent')).toBeNull();
+ });
+ });
+});
+
+// ============================================================================
+// Tests de TravellerNpcError
+// ============================================================================
+
+describe('TravellerNpcError', () => {
+ it('devrait créer une erreur avec message et code', () => {
+ const error = new TravellerNpcError('Test error', 'TEST_ERROR');
+ expect(error.message).toBe('Test error');
+ expect(error.code).toBe('TEST_ERROR');
+ expect(error.name).toBe('TravellerNpcError');
+ expect(error.isTravellerNpcError).toBe(true);
+ expect(error.details).toEqual({});
+ expect(error.timestamp).toBeDefined();
+ });
+
+ it('devrait créer une erreur avec des détails', () => {
+ const details = { foo: 'bar' };
+ const error = new TravellerNpcError('Test error', 'TEST_ERROR', details);
+ expect(error.details).toEqual(details);
+ });
+
+ it('devrait créer une erreur à partir d\'une erreur existante', () => {
+ const original = new Error('Original error');
+ const error = TravellerNpcError.from(original, 'WRAPPED_ERROR');
+
+ expect(error.message).toBe('Original error');
+ expect(error.code).toBe('WRAPPED_ERROR');
+ expect(error.details.originalError).toBe(original);
+ });
+
+ it('devrait retourner l\'erreur existante si c\'est déjà une TravellerNpcError', () => {
+ const original = new TravellerNpcError('Original', 'ORIG');
+ const error = TravellerNpcError.from(original, 'WRAPPED');
+
+ expect(error).toBe(original);
+ });
+
+ it('devrait gérer undefined comme erreur originale', () => {
+ const error = TravellerNpcError.from(undefined, 'UNKNOWN');
+ expect(error.message).toBe('Erreur inconnue');
+ expect(error.code).toBe('UNKNOWN');
+ });
+});
+
+// ============================================================================
+// Tests des données de configuration
+// ============================================================================
+
+describe('Données de configuration', () => {
+ describe('CITIZEN_CATEGORY_LIST', () => {
+ it('devrait contenir toutes les catégories', () => {
+ expect(CITIZEN_CATEGORY_LIST).toHaveLength(4);
+ expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.BELOW_AVERAGE);
+ expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.AVERAGE);
+ expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.ABOVE_AVERAGE);
+ expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.EXCEPTIONAL);
+ });
+ });
+
+ describe('EXPERIENCE_LEVEL_LIST', () => {
+ it('devrait contenir tous les niveaux', () => {
+ expect(EXPERIENCE_LEVEL_LIST).toHaveLength(6);
+ expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.RECRUIT);
+ expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.ROOKIE);
+ expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.INTERMEDIATE);
+ expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.REGULAR);
+ expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.VETERAN);
+ expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.ELITE);
+ });
+ });
+
+ describe('ROLE_LIST', () => {
+ it('devrait contenir tous les rôles', () => {
+ expect(ROLE_LIST.length).toBeGreaterThan(10);
+ expect(ROLE_LIST).toContain(ROLE.PILOT);
+ expect(ROLE_LIST).toContain(ROLE.NAVIGATOR);
+ expect(ROLE_LIST).toContain(ROLE.ENGINEER);
+ expect(ROLE_LIST).toContain(ROLE.MEDIC);
+ });
+ });
+
+ describe('GENDER_LIST', () => {
+ it('devrait contenir tous les genres', () => {
+ expect(GENDER_LIST).toHaveLength(3);
+ expect(GENDER_LIST).toContain(GENDER.UNSPECIFIED);
+ expect(GENDER_LIST).toContain(GENDER.FEMALE);
+ expect(GENDER_LIST).toContain(GENDER.MALE);
+ });
+ });
+
+ describe('CHARACTERISTIC_LIST', () => {
+ it('devrait contenir toutes les caractéristiques', () => {
+ expect(CHARACTERISTIC_LIST).toHaveLength(6);
+ expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.STR);
+ expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.DEX);
+ expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.END);
+ expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.INT);
+ expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.EDU);
+ expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.SOC);
+ });
+ });
+
+ describe('UPP_ORDER', () => {
+ it('devrait contenir l\'ordre correct des caractéristiques', () => {
+ expect(UPP_ORDER).toEqual(['STR', 'DEX', 'END', 'INT', 'EDU', 'SOC']);
+ });
+ });
+
+ describe('ERROR_CODES', () => {
+ it('devrait contenir tous les codes d\'erreur', () => {
+ expect(ERROR_CODES.INVALID_OPTIONS).toBe('INVALID_OPTIONS');
+ expect(ERROR_CODES.INVALID_ROLE).toBe('INVALID_ROLE');
+ expect(ERROR_CODES.INVALID_CATEGORY).toBe('INVALID_CATEGORY');
+ expect(ERROR_CODES.INVALID_EXPERIENCE).toBe('INVALID_EXPERIENCE');
+ expect(ERROR_CODES.INVALID_GENDER).toBe('INVALID_GENDER');
+ expect(ERROR_CODES.ACTOR_CREATION_FAILED).toBe('ACTOR_CREATION_FAILED');
+ expect(ERROR_CODES.MGT2E_NOT_ACTIVE).toBe('MGT2E_NOT_ACTIVE');
+ expect(ERROR_CODES.BASE_ACTOR_NOT_FOUND).toBe('BASE_ACTOR_NOT_FOUND');
+ });
+ });
+
+ describe('DEFAULT_OPTIONS', () => {
+ it('devrait avoir des valeurs par défaut valides', () => {
+ expect(DEFAULT_OPTIONS.citizenCategory).toBe('average');
+ expect(DEFAULT_OPTIONS.experience).toBe('regular');
+ expect(DEFAULT_OPTIONS.role).toBe('pilot');
+ expect(DEFAULT_OPTIONS.gender).toBe('unspecified');
+ expect(DEFAULT_OPTIONS.createActor).toBe(false);
+ expect(DEFAULT_OPTIONS.openCreatedActor).toBe(true);
+ });
+ });
+
+ describe('NAME_CATALOGS', () => {
+ it('devrait avoir des catalogues de noms définis', () => {
+ expect(NAME_CATALOGS.surnames).toBeDefined();
+ expect(NAME_CATALOGS.surnames.length).toBeGreaterThan(0);
+ expect(NAME_CATALOGS.nonGenderedNames).toBeDefined();
+ expect(NAME_CATALOGS.nonGenderedNames.length).toBeGreaterThan(0);
+ expect(NAME_CATALOGS.femaleNames).toBeDefined();
+ expect(NAME_CATALOGS.femaleNames.length).toBeGreaterThan(0);
+ expect(NAME_CATALOGS.maleNames).toBeDefined();
+ expect(NAME_CATALOGS.maleNames.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('CHARACTERISTIC_PRIORITIES', () => {
+ it('devrait avoir des priorités pour chaque rôle', () => {
+ expect(CHARACTERISTIC_PRIORITIES.pilot).toBeDefined();
+ expect(CHARACTERISTIC_PRIORITIES.navigator).toBeDefined();
+ expect(CHARACTERISTIC_PRIORITIES.engineer).toBeDefined();
+ expect(CHARACTERISTIC_PRIORITIES.medic).toBeDefined();
+ expect(CHARACTERISTIC_PRIORITIES.marine).toBeDefined();
+ });
+
+ it('devrait avoir des groupes high, medium, low', () => {
+ const priorities = CHARACTERISTIC_PRIORITIES.pilot;
+ expect(priorities.high).toBeDefined();
+ expect(priorities.medium).toBeDefined();
+ expect(priorities.low).toBeDefined();
+ expect(priorities.high.length + priorities.medium.length + priorities.low.length).toBe(6);
+ });
+ });
+
+ describe('ROLE_SKILLS', () => {
+ it('devrait avoir des compétences pour chaque rôle', () => {
+ expect(ROLE_SKILLS.pilot).toBeDefined();
+ expect(ROLE_SKILLS.navigator).toBeDefined();
+ expect(ROLE_SKILLS.engineer).toBeDefined();
+ expect(ROLE_SKILLS.medic).toBeDefined();
+ expect(ROLE_SKILLS.marine).toBeDefined();
+ });
+
+ it('devrait avoir au moins 10 compétences par rôle', () => {
+ Object.values(ROLE_SKILLS).forEach(skills => {
+ expect(skills.length).toBeGreaterThanOrEqual(10);
+ });
+ });
+ });
+});
+
+// ============================================================================
+// Tests d\'intégration (si environnement Foundry disponible)
+// ============================================================================
+
+// Ces tests sont optionnels et nécessitent un environnement Foundry complet
+// Ils sont marqués comme skip par défaut
+
+describe.skip('Intégration Foundry (nécessite environnement Foundry)', () => {
+ describe('buildMgt2eCharacteristics', () => {
+ it('devrait construire les caractéristiques au format mgt2e', () => {
+ const existing = {};
+ const characteristics = {
+ STR: 8,
+ DEX: 9,
+ END: 7,
+ INT: 10,
+ EDU: 8,
+ SOC: 6
+ };
+
+ const result = buildMgt2eCharacteristics(existing, characteristics);
+
+ expect(result.STR).toBeDefined();
+ expect(result.STR.value).toBe(8);
+ expect(result.STR.dm).toBe(calculateDm(8));
+ });
+ });
+
+ describe('buildMgt2eSkills', () => {
+ it('devrait construire les compétences au format mgt2e', () => {
+ const existing = {};
+ const skills = [
+ { name: 'Pilot-Spacecraft', level: 2 },
+ { name: 'Gun Combat', level: 1 }
+ ];
+
+ const result = buildMgt2eSkills(existing, skills);
+
+ expect(result).toBeDefined();
+ });
+ });
+});
+
+// ============================================================================
+// Export pour les tests manuels
+// ============================================================================
+
+if (typeof globalThis !== 'undefined') {
+ globalThis.TravellerNpcGeneratorTests = {
+ generateCharacteristics,
+ generateSkills,
+ generateTravellerNpc,
+ validateOptions,
+ toHex,
+ calculateDm,
+ pickRandomItem,
+ shuffleArray,
+ TravellerNpcError,
+ ModuleCache,
+ runAllTests: () => {
+ console.log('Exécutant les tests du générateur de PNJ Traveller...');
+ console.log('Tous les tests sont définis. Exécutez avec Jest pour voir les résultats.');
+ }
+ };
+}
diff --git a/scripts/travellerNpcGenerator.js b/scripts/travellerNpcGenerator.js
index 1edc791..4d6f51d 100644
--- a/scripts/travellerNpcGenerator.js
+++ b/scripts/travellerNpcGenerator.js
@@ -7,23 +7,17 @@
import {
CITIZEN_CATEGORY,
- CITIZEN_CATEGORY_LIST,
EXPERIENCE_LEVEL,
- EXPERIENCE_LEVEL_LIST,
ROLE,
- ROLE_LIST,
ROLE_SKILLS,
CHARACTERISTIC_PRIORITIES,
GENDER,
- GENDER_LIST,
CHARACTERISTIC,
CHARACTERISTIC_LIST,
UPP_ORDER,
- NAME_CATALOGS,
toHex,
calculateDm,
pickRandomItem,
- popRandomItems,
shuffleArray,
getRoleByKey,
getCitizenCategoryByKey,
@@ -31,15 +25,115 @@ import {
getGenderByKey,
getSkillsForRole,
getCharacteristicPrioritiesForRole,
- DEFAULT_OPTIONS
+ validateOptions,
+ DEFAULT_OPTIONS,
+ NAME_CATALOGS,
+ SKILL_LABELS_FR,
+ CHARACTERISTIC_LABELS_FR,
+ CITIZEN_CATEGORY_LABELS_FR,
+ EXPERIENCE_LEVEL_LABELS_FR,
+ ROLE_LABELS_FR,
+ GENDER_LABELS_FR
} from './data/travellerNpcGenerator.js';
import { setSkillLevel, localizeSkill } from './mgt2eSkills.js';
+import { travellerNpcCache, TravellerNpcError, ERROR_CODES } from './utils/travellerNpcUtils.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
-// Cache pour le système de base des acteurs mgt2e
-let mgt2eBaseActorSystemPromise = null;
+// ============================================================================
+// Conversion des compétences
+// ============================================================================
+
+/**
+ * Mapping des compétences Traveller vers mgt2e
+ * @type {Object}
+ */
+const SKILL_MAPPING = {
+ 'Pilot-Spacecraft': 'pilot.spacecraft',
+ 'Pilot-Small Craft': 'pilot.smallcraft',
+ 'Pilot': 'pilot',
+ 'Astrogation': 'astrogation',
+ 'Electronics-Sensors': 'electronics.sensors',
+ 'Electronics-Communications': 'electronics.communications',
+ 'Electronics-Computers': 'electronics.computers',
+ 'Electronics': 'electronics',
+ 'Gunner-Turrets': 'gunner.turrets',
+ 'Gunner-Screens': 'gunner.screens',
+ 'Gunner': 'gunner',
+ 'Mechanic': 'mechanic',
+ 'Engineer-MDrive': 'engineer.mdrive',
+ 'Engineer-Power': 'engineer.power',
+ 'Engineer-JDrive': 'engineer.jdrive',
+ 'Engineer-Life Support': 'engineer.lifesupport',
+ 'Engineer': 'engineer',
+ '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',
+ 'Science': 'science',
+ 'Deception': 'deception',
+ 'Investigate': 'investigate',
+ 'Gun Combat': 'guncombat',
+ 'Heavy Weapons': 'heavyweapons',
+ 'Melee-Unarmed': 'melee.unarmed',
+ 'Melee-Blade': 'melee.blade',
+ 'Melee': 'melee',
+ 'Athletics-Strength': 'athletics.strength',
+ 'Athletics-Dexterity': 'athletics.dexterity',
+ 'Athletics': 'athletics',
+ 'Tactics': 'tactics',
+ 'Recon': 'recon',
+ 'Survival': 'survival',
+ 'Navigation': 'navigation',
+ 'Stealth': 'stealth',
+ 'Explosives': 'explosives',
+ 'Communications': 'electronics.communications',
+ 'Drive-Grav': 'drive.grav',
+ 'Drive': 'drive',
+ 'Vacc Suit': 'vaccsuit',
+ 'Flyer': 'flyer',
+ 'Art-Acting': 'art.acting',
+ 'Art-Instrument': 'art.instrument',
+ 'Art': 'art'
+};
+
+/**
+ * Convertit un nom de compétence du format Traveller vers le format mgt2e
+ * @param {string} skillName - Nom de la compétence
+ * @returns {string} - Nom au format mgt2e
+ */
+function convertSkillToMgt2eFormat(skillName) {
+ return SKILL_MAPPING[skillName] || skillName.toLowerCase().replace(/-| /g, '.');
+}
+
+/**
+ * Obtient le libellé français d'une compétence Traveller
+ * @param {string} skillName - Nom de la compétence (ex: 'Pilot-Spacecraft')
+ * @returns {string} - Libellé français
+ */
+function getSkillLabelFr(skillName) {
+ return SKILL_LABELS_FR[skillName] || skillName;
+}
+
+/**
+ * Obtient le libellé français d'une caractéristique
+ * @param {string} charKey - Clé de la caractéristique (ex: 'STR')
+ * @returns {string} - Libellé français
+ */
+function getCharacteristicLabelFr(charKey) {
+ return CHARACTERISTIC_LABELS_FR[charKey] || charKey;
+}
// ============================================================================
// Génération des caractéristiques
@@ -56,36 +150,34 @@ export function generateCharacteristics(citizenCategoryKey, roleKey) {
const category = getCitizenCategoryByKey(citizenCategoryKey);
const priorities = getCharacteristicPrioritiesForRole(roleKey);
- // On commence avec l'array de base de la catégorie
- let characteristicArray = [...category.characteristicArray];
+ // Cloner et mélanger l'array de base de la catégorie
+ let characteristicArray = shuffleArray([...category.characteristicArray]);
- // On attribue les valeurs aux caractéristiques selon les priorités du rôle
const characteristics = {};
- // 1. Attribuer les valeurs les plus élevées aux caractéristiques High priority
- const [highValues, remaining1] = popRandomItems(characteristicArray, priorities.high.length);
- priorities.high.forEach((charKey, index) => {
- characteristics[charKey] = highValues[index] || 7;
- });
+ // Créer un tableau de priorités avec leur ordre
+ const priorityGroups = [
+ { keys: priorities.high, count: priorities.high.length },
+ { keys: priorities.medium, count: priorities.medium.length },
+ { keys: priorities.low, count: priorities.low.length }
+ ];
- // 2. Attribuer les valeurs moyennes aux caractéristiques Medium priority
- const [mediumValues, remaining2] = popRandomItems(remaining1, priorities.medium.length);
- priorities.medium.forEach((charKey, index) => {
- characteristics[charKey] = mediumValues[index] || 7;
- });
+ // Attribuer les valeurs dans l'ordre de priorité
+ for (const group of priorityGroups) {
+ const values = characteristicArray.slice(0, group.count);
+ characteristicArray = characteristicArray.slice(group.count);
+
+ group.keys.forEach((charKey, i) => {
+ characteristics[charKey] = values[i] ?? 7;
+ });
+ }
- // 3. Attribuer les valeurs restantes aux caractéristiques Low priority
- const [lowValues] = popRandomItems(remaining2, priorities.low.length);
- priorities.low.forEach((charKey, index) => {
- characteristics[charKey] = lowValues[index] || 7;
- });
-
- // S'assurer que toutes les caractéristiques sont définies
- UPP_ORDER.forEach(charKey => {
+ // Remplir les valeurs manquantes avec 7
+ for (const charKey of UPP_ORDER) {
if (characteristics[charKey] === undefined) {
characteristics[charKey] = 7;
}
- });
+ }
// Construire l'UPP
const upp = UPP_ORDER.map(charKey => toHex(characteristics[charKey])).join('');
@@ -105,85 +197,55 @@ export function generateCharacteristics(citizenCategoryKey, roleKey) {
* Distribue les niveaux de compétence sur une liste de compétences
*
* @param {string[]} roleSkills - Liste des compétences du rôle
- * @param {Object} distribution - Distribution des niveaux (level0, level1, level2, level3)
- * @returns {Map} - Map des compétences avec leurs niveaux
+ * @param {Object} distribution - Distribution des niveaux
+ * @returns {Array<{name: string, level: number}>} - Compétences avec niveaux
*/
function distributeSkillLevels(roleSkills, distribution) {
- const skillLevels = new Map();
- const maxLevelBySkill = new Map();
-
- // 1. On commence par les compétences de niveau 3 (les plus rares)
- let remainingSkills = [...roleSkills];
-
- // Supprimer les doublons (spécialisations)
+ // Supprimer les doublons EXACTS (même nom complet de compétence)
+ // mais conserver les spécialisations comme Pilot-Spacecraft et Pilot-Small Craft
const uniqueSkills = [];
- const seenSkills = new Set();
- for (const skill of remainingSkills) {
- const baseSkill = skill.split('-')[0];
- if (!seenSkills.has(baseSkill)) {
- seenSkills.add(baseSkill);
+ const seen = new Set();
+
+ for (const skill of roleSkills) {
+ if (!seen.has(skill)) {
+ seen.add(skill);
uniqueSkills.push(skill);
}
}
- remainingSkills = uniqueSkills;
- // Level 3
- if (distribution.level3 > 0) {
- const [level3Skills, remaining] = popRandomItems(remainingSkills, distribution.level3);
- level3Skills.forEach(skill => {
- skillLevels.set(skill, 3);
- maxLevelBySkill.set(skill, 3);
- });
- remainingSkills = remaining;
+ // Mélanger les compétences une seule fois
+ const shuffledSkills = shuffleArray([...uniqueSkills]);
+
+ // Créer un tableau de niveaux, initialisé à 0
+ const levels = new Array(shuffledSkills.length).fill(0);
+
+ // Distribuer les niveaux de manière séquentielle
+ let index = 0;
+
+ // Level 3 (les plus rares, en premier)
+ const level3Count = Math.min(distribution.level3, shuffledSkills.length);
+ for (let i = 0; i < level3Count; i++) {
+ if (index < levels.length) levels[index++] = 3;
}
// Level 2
- if (distribution.level2 > 0) {
- const [level2Skills, remaining] = popRandomItems(remainingSkills, distribution.level2);
- level2Skills.forEach(skill => {
- const currentLevel = skillLevels.get(skill) || 0;
- const newLevel = Math.max(currentLevel, 2);
- skillLevels.set(skill, newLevel);
- if (newLevel > (maxLevelBySkill.get(skill) || 0)) {
- maxLevelBySkill.set(skill, newLevel);
- }
- });
- remainingSkills = remaining;
+ const level2Count = Math.min(distribution.level2, shuffledSkills.length - level3Count);
+ for (let i = 0; i < level2Count; i++) {
+ if (index < levels.length) levels[index++] = 2;
}
// Level 1
- if (distribution.level1 > 0) {
- const [level1Skills, remaining] = popRandomItems(remainingSkills, distribution.level1);
- level1Skills.forEach(skill => {
- const currentLevel = skillLevels.get(skill) || 0;
- const newLevel = Math.max(currentLevel, 1);
- skillLevels.set(skill, newLevel);
- if (newLevel > (maxLevelBySkill.get(skill) || 0)) {
- maxLevelBySkill.set(skill, newLevel);
- }
- });
- remainingSkills = remaining;
+ const level1Count = Math.min(distribution.level1, shuffledSkills.length - level3Count - level2Count);
+ for (let i = 0; i < level1Count; i++) {
+ if (index < levels.length) levels[index++] = 1;
}
- // Level 0 - les compétences restantes
- if (distribution.level0 > 0) {
- const [level0Skills] = popRandomItems(remainingSkills, distribution.level0);
- level0Skills.forEach(skill => {
- const currentLevel = skillLevels.get(skill) || 0;
- if (currentLevel === 0) {
- skillLevels.set(skill, 0);
- }
- });
- }
+ // Level 0 (déjà initialisé)
- // Pour les compétences restantes non sélectionnées, leur donner niveau 0
- for (const skill of roleSkills) {
- if (!skillLevels.has(skill)) {
- skillLevels.set(skill, 0);
- }
- }
-
- return skillLevels;
+ // Créer le résultat trié par niveau (descendant) puis par nom
+ return shuffledSkills
+ .map((skill, i) => ({ name: skill, level: levels[i] }))
+ .sort((a, b) => b.level - a.level || a.name.localeCompare(b.name));
}
/**
@@ -198,135 +260,47 @@ export function generateSkills(roleKey, experienceKey) {
const experience = getExperienceLevelByKey(experienceKey);
const distribution = experience.skillDistribution;
- const skillLevels = distributeSkillLevels(roleSkills, distribution);
-
- // Convertir en tableau trié par niveau (descendant) puis par nom
- const skills = Array.from(skillLevels.entries())
- .sort((a, b) => {
- // D'abord par niveau (descendant)
- if (b[1] !== a[1]) {
- return b[1] - a[1];
- }
- // Puis par nom (ascendant)
- return a[0].localeCompare(b[0]);
- })
- .map(([name, level]) => ({ name, level }));
-
- return skills;
-}
-
-// ============================================================================
-// Génération du nom
-// ============================================================================
-
-/**
- * Génère un nom aléatoire
- *
- * @param {string} genderKey - Clé du genre
- * @returns {{firstName: string, surname: string, fullName: string}}
- */
-export function generateName(genderKey) {
- return pickRandomItem(NAME_CATALOGS.nonGenderedNames) + ' ' + pickRandomItem(NAME_CATALOGS.surnames);
+ return distributeSkillLevels(roleSkills, distribution);
}
// ============================================================================
// Génération complète du PNJ
// ============================================================================
-/**
- * Convertit un nom de compétence du format Traveller vers le format mgt2e
- * @param {string} skillName - Nom de la compétence (ex: "Pilot-Spacecraft")
- * @returns {string} - Nom de la compétence au format mgt2e (ex: "pilot.spacecraft")
- */
-function convertSkillToMgt2eFormat(skillName) {
- // Remplacer les tirets par des points
- // Note: Certaines compétences ont des spécialisations comme "Pilot-Small Craft" -> "pilot.smallcraft"
- const mapping = {
- 'Pilot-Spacecraft': 'pilot.spacecraft',
- 'Pilot-Small Craft': 'pilot.smallcraft',
- 'Pilot': 'pilot',
- 'Astrogation': 'astrogation',
- 'Electronics-Sensors': 'electronics.sensors',
- 'Electronics-Communications': 'electronics.communications',
- 'Electronics-Computers': 'electronics.computers',
- 'Electronics': 'electronics',
- 'Gunner-Turrets': 'gunner.turrets',
- 'Gunner-Screens': 'gunner.screens',
- 'Gunner': 'gunner',
- 'Mechanic': 'mechanic',
- 'Engineer-MDrive': 'engineer.mdrive',
- 'Engineer-Power': 'engineer.power',
- 'Engineer-JDrive': 'engineer.jdrive',
- 'Engineer-Life Support': 'engineer.lifesupport',
- 'Engineer': 'engineer',
- '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',
- 'Science': 'science',
- 'Deception': 'deception',
- 'Investigate': 'investigate',
- 'Gun Combat': 'guncombat',
- 'GunCombat': 'guncombat',
- 'Heavy Weapons': 'heavyweapons',
- 'HeavyWeapons': 'heavyweapons',
- 'Melee-Unarmed': 'melee.unarmed',
- 'Melee-Blade': 'melee.blade',
- 'Melee': 'melee',
- 'Athletics-Strength': 'athletics.strength',
- 'Athletics-Dexterity': 'athletics.dexterity',
- 'Athletics': 'athletics',
- 'Tactics': 'tactics',
- 'Recon': 'recon',
- 'Survival': 'survival',
- 'Navigation': 'navigation',
- 'Stealth': 'stealth',
- 'Explosives': 'explosives',
- 'Communications': 'electronics.communications',
- 'Drive-Grav': 'drive.grav',
- 'Drive': 'drive',
- 'Vacc Suit': 'vaccsuit',
- 'VaccSuit': 'vaccsuit',
- 'Flyer': 'flyer',
- 'Art-Acting': 'art.acting',
- 'Art-Instrument': 'art.instrument',
- 'Art': 'art',
- 'Flyer': 'flyer',
- 'Engineer-Manoeuvre Drive': 'engineer.manoeuvredrive',
- 'Engineer-Manoeuvre': 'engineer.manoeuvredrive'
- };
-
- return mapping[skillName] || skillName.toLowerCase().replace(/-| /g, '.');
-}
-
/**
* Génère un PNJ Traveller complet
*
- * @param {Object} options - Options de génération
- * @param {string} [options.citizenCategory='average'] - Catégorie de citoyen
- * @param {string} [options.experience='regular'] - Niveau d'expérience
- * @param {string} [options.role='pilot'] - Rôle
- * @param {string} [options.gender='unspecified'] - Genre
- * @param {string} [options.firstName] - Prénom forcé
- * @param {string} [options.surname] - Nom de famille forcé
- * @returns {Object} - PNJ généré
+ * @typedef {Object} TravellerNpcOptions
+ * @property {string} [citizenCategory] - Catégorie de citoyen
+ * @property {string} [experience] - Niveau d'expérience
+ * @property {string} [role] - Rôle
+ * @property {string} [gender] - Genre
+ * @property {string} [firstName] - Prénom forcé
+ * @property {string} [surname] - Nom de famille forcé
+ *
+ * @typedef {Object} TravellerNpcResult
+ * @property {boolean} success - Succès de la génération
+ * @property {string} type - Type de résultat
+ * @property {Object} name - Nom du PNJ
+ * @property {Object} role - Rôle
+ * @property {Object} citizenCategory - Catégorie
+ * @property {Object} experience - Expérience
+ * @property {Object} gender - Genre
+ * @property {Object} characteristics - Caractéristiques
+ * @property {string} upp - Code UPP
+ * @property {Array<{name: string, level: number}>} skills - Compétences
+ * @property {Array<{name: string, level: number}>} skillsForActor - Compétences pour mgt2e
+ * @property {Object} display - Métadonnées pour l'affichage
+ *
+ * @param {TravellerNpcOptions} [options={}]
+ * @returns {TravellerNpcResult}
*/
export function generateTravellerNpc(options = {}) {
- // Fusionner avec les options par défaut
- const opts = {
+ // Valider et fusionner avec les options par défaut
+ const opts = validateOptions({
...DEFAULT_OPTIONS,
...options
- };
+ });
// Générer le nom
let name;
@@ -337,16 +311,17 @@ export function generateTravellerNpc(options = {}) {
fullName: `${opts.firstName} ${opts.surname}`
};
} else {
+ const firstName = pickRandomItem(
+ opts.gender === 'female' ? NAME_CATALOGS.femaleNames :
+ opts.gender === 'male' ? NAME_CATALOGS.maleNames :
+ NAME_CATALOGS.nonGenderedNames
+ );
+ const surname = pickRandomItem(NAME_CATALOGS.surnames);
name = {
- firstName: pickRandomItem(
- opts.gender === 'female' ? NAME_CATALOGS.femaleNames :
- opts.gender === 'male' ? NAME_CATALOGS.maleNames :
- NAME_CATALOGS.nonGenderedNames
- ),
- surname: pickRandomItem(NAME_CATALOGS.surnames),
- fullName: ''
+ firstName,
+ surname,
+ fullName: `${firstName} ${surname}`
};
- name.fullName = `${name.firstName} ${name.surname}`;
}
// Générer les caractéristiques
@@ -370,12 +345,18 @@ export function generateTravellerNpc(options = {}) {
const role = getRoleByKey(opts.role);
const gender = getGenderByKey(opts.gender);
- // Libellés des caractéristiques pour l'affichage
+ // Libellés des caractéristiques pour l'affichage (en français)
const characteristicLabels = {};
CHARACTERISTIC_LIST.forEach(char => {
- characteristicLabels[char.key] = char.label;
+ characteristicLabels[char.key] = getCharacteristicLabelFr(char.key);
});
+ // Ajouter les libellés français aux compétences pour l'affichage
+ const skillsWithLabels = skills.map(skill => ({
+ ...skill,
+ labelFr: getSkillLabelFr(skill.name)
+ }));
+
return {
success: true,
type: 'traveller-npc',
@@ -386,66 +367,103 @@ export function generateTravellerNpc(options = {}) {
gender,
characteristics,
upp,
- skills,
- skillsForActor, // Compétences au format mgt2e
+ skills: skillsWithLabels,
+ skillsForActor,
MODULE_ID,
UPP_ORDER,
- // Métadonnées pour l'affichage
display: {
- roleLabel: role.label,
- categoryLabel: citizenCategory.label,
- experienceLabel: experience.label,
- genderLabel: gender.label,
+ roleLabel: ROLE_LABELS_FR[role.key] || role.label,
+ categoryLabel: CITIZEN_CATEGORY_LABELS_FR[citizenCategory.key] || citizenCategory.label,
+ experienceLabel: EXPERIENCE_LEVEL_LABELS_FR[experience.key] || experience.label,
+ genderLabel: GENDER_LABELS_FR[gender.key] || gender.label,
characteristicLabels
}
};
}
// ============================================================================
-// Création de la fiche d'acteur
+// Récupération du système de base mgt2e
// ============================================================================
/**
* Récupère le système de base des acteurs mgt2e
* @returns {Promise