fix: corrections générateur PNJ Traveller et tests Jest

- validateOptions : remplit les champs manquants et corrige les clés invalides
  (détection précédemment inopérante car getXByKey renvoie l'objet par défaut)
- generateCharacteristics : ne mélange plus le tableau avant affectation,
  la priorité des caractéristiques (high → medium → low) est respectée
- Exports manquants : convertSkillToMgt2eFormat, popRandomItems, ERROR_CODES,
  TravellerNpcError
- Tests : import des globals via @jest/globals (obligatoire en ESM),
  suppression de l'import obsolète buildMgt2eBaseActorSystem,
  attente corrigée pilot.smallCraft, test des doublons aligné sur le
  dédoublonnage réel (noms complets, spécialisations conservées)
- AGENTS.md : section Testing mise à jour
- README.md : ajouté
This commit is contained in:
2026-08-03 21:02:35 +02:00
parent f4ba85bcb9
commit 1c55484bf7
5 changed files with 188 additions and 20 deletions
+11 -6
View File
@@ -38,15 +38,20 @@ Commands are registered via `ChatLogV2.CHAT_COMMANDS` — not Hooks.on("chatMess
## Testing ## Testing
Deux fichiers de test, pas de build :
```sh ```sh
# standalone, no npm needed — install jest globally or use npx # Tests Jest (globals importés via @jest/globals, flag ESM requis)
npx jest scripts/tests/travellerNpcGenerator.test.js NODE_OPTIONS="--experimental-vm-modules" npx jest scripts/tests/travellerNpcGenerator.test.js
# Mini-runner maison (Jest rapporte un faux positif « at least one test »)
node scripts/tests/allyEnemyGenerator.test.js
``` ```
- Single file, 1096 lines, hand-rolled FoundryVTT mocks. - `travellerNpcGenerator.test.js` : Jest (`describe`/`it`/`expect`), mock FoundryVTT fait main, ~1100 lignes. Couvre : utilitaires, lookups, validation, caractéristiques/compétences, conversion de compétences, génération complète, ModuleCache, erreurs, config validation.
- Covers: utilities, lookups, validation, characteristic/skill generation, skill conversion, full NPC gen, ModuleCache, errors, config validation. - `allyEnemyGenerator.test.js` : son propre runner (`test()`/`assertEqual`), 39 tests, à lancer avec `node` directement.
- No CI test step — you must run manually. - Config Jest minimale dans `jest.config.cjs` (`testMatch` + `transform: {}`).
- No Jest config file — relies on defaults. - Pas de CI test step — à lancer manuellement.
## Versioning ## Versioning
+147
View File
@@ -0,0 +1,147 @@
# MgT2e — Compendium Amiral Denisov
Module **FoundryVTT** pour **Mongoose Traveller 2e (MgT2e)**, regroupant des compendiums de référence et une suite d'outils d'aide au MJ (commerce, PNJ, rencontres, missions, cartes stellaires, calendrier impérial).
**Compatibilité :** FoundryVTT v13 et v14
---
## ✨ Fonctionnalités
### Commandes de chat
| Commande | Rôle |
|---|---|
| `/commerce` | Génération d'offres de commerce (3 onglets) |
| `/pnj` | Génération de PNJ rapide (4 onglets) |
| `/rencontre` | Rencontres aléatoires |
| `/mission` | Contrats / missions aléatoires |
| `/sector` | Carte stellaire interactive (Traveller Map) |
| `/subsector` | Carte d'un sous-secteur |
| `/cal` | Fenêtre du calendrier impérial |
### Génération de PNJ
- **PNJ rapide** : nom, carrière, caractéristiques, compétences (localisées en français), relations
- **PNJ détaillé (Traveller)** : génération selon les règles du générateur officiel, création de fiche `npc` MgT2e avec spécialités de compétences natives
- **Alliés & Ennemis** : relations générées via tables D66, modificateurs d'affinité/inimité, caractéristiques spéciales
- **Création d'acteur** : possibilité de créer directement une fiche PNJ et de l'ouvrir
### Import par lot de PNJ
- **`scripts/batchNpcCreator.js`** : importe un répertoire d'images (fichiers) comme PNJs, grille de raccourcis dans la barre latérale *Acteurs* (Commerce, PNJ, Rencontre, Mission), bouton *Traveller Map* dans la barre latérale *Scènes*
### Carte stellaire (Traveller Map)
- Carte interactive en IFRAME avec recherche de mondes
- Carte de chat complète : UWP (avec détail), importance, économie, culture, population, noblesse, bases, remarques, allégeance, étoiles
- **Synchronisation** : le MJ peut partager la vue actuelle (hex, zoom) avec tous les joueurs via socket
- **Vue joueur** en lecture seule, mise à jour en temps réel
- **Synchronisation du canvas** : le MJ peut aligner la vue des joueurs sur la sienne
### Calendrier impérial
- Fenêtre compacte affichant la date impériale (`Wonday, 234-1116 IC ─ 14:30`)
- **Configurable par le MJ** : année, jour (1-365), heure:minute locale, planète
- Boutons rapides `+1 jour` et `+1 heure` (report 24 h → jour, 365 → 1)
- **Conversions calendaires** : Solomani, Vilani, Zhodani (Olympiades), Aslan, K'Kree, Hiver (formules approximatives de [Date_Conversion](https://wiki.travellerrpg.com/Date_Conversion))
- **Synchronisé pour les joueurs** en temps réel via socket
- **Auto-ouverture** au démarrage (désactivable) et **sauvegarde de la position** de la fenêtre
- **Notes de bord** : bouton crayon → saisie d'une note datée (date impériale + date réelle), ajoutée à un journal de session
### Compendiums
14 compendiums organisés en dossiers thématiques :
| Dossier | Contenu |
|---|---|
| **Équipements** | armures, objets, équipement, ordinateur, contenants, armes |
| **Références** | compétences, maladie/poison/blessure, espèces, talents psioniques, carrières |
| **PNJ & Items** | tables PNJ, PNJ, compétences, maladies, espèces, talents, carrières |
| **Journaux** | journal psioniques |
---
## 📦 Installation
1. **Copie manuelle** : placer le dossier dans `FoundryVTT/Data/modules/mgt2-compendium-amiral-denisov/`
2. **Manifest** : dans FoundryVTT, *Add-on Modules → Install Module* avec l'URL du manifest
Activer ensuite le module dans la configuration du monde.
---
## 🎮 Utilisation
### Commencer une partie
Tapez une commande dans le chat (ex. `/pnj`) ou utilisez les raccourcis de la barre latérale. Le message d'accueil au lancement rappelle les commandes disponibles.
### Calendrier impérial
1. `/cal` (ou auto-ouverture au démarrage)
2. Clic sur la roue dentée **⚙** (MJ) pour configurer la date
3. Clic sur le crayon **✎** (MJ) pour ajouter une note au journal de bord
### Paramètres du monde
Dans *Configuration → Réglages → Module Settings* :
| Réglage | Description |
|---|---|
| **Calendrier impérial : auto-ouverture** | Ouvrir automatiquement la fenêtre au démarrage |
| **Journal de bord** | Nom du journal utilisé pour les notes de session (créé automatiquement) |
---
## 🛠 Développement
### Structure
```
module.json Manifeste du module (source de vérité pour la version)
scripts/
data/ Tables de données (NPC, commerce, alliés/ennemis, générateur Traveller)
tests/ Tests unitaires (Jest)
*.js Logique des outils
*.css → styles/ Styles
templates/ Templates Handlebars
packs/ Compendiums (niveau de base de données)
```
### Prérequis
- Aucun gestionnaire de paquets, aucun build — JavaScript ESM chargé directement par FoundryVTT
- `package.json` créé uniquement pour les tests (`"type": "module"`)
### Tests
Deux styles de tests coexistent (pas de configuration Jest, `package.json` n'existe que pour `"type": "module"` + mock FoundryVTT fait main) :
- **`allyEnemyGenerator.test.js`** — mini-runner maison (`test()`/`assertEqual`), à lancer avec Node directement :
```sh
node scripts/tests/allyEnemyGenerator.test.js # "All N tests passed"
```
- **`travellerNpcGenerator.test.js`** — Jest (`describe`/`it`), à lancer avec le flag ESM :
```sh
NODE_OPTIONS="--experimental-vm-modules" npx jest scripts/tests/travellerNpcGenerator.test.js
```
### Conventions
- Tout le contenu (libellés, commentaires, messages de commit) est en **français**
- Dialogs : `ApplicationV2` + `HandlebarsApplicationMixin`
- Compatibilité v13/v14 : certains hooks doivent gérer les API jQuery (v13) et DOM (v14)
- Dés à lancer : toujours `await new Roll(formula).evaluate()`
---
## 📄 Licence & Crédits
- Contenu Traveller © Mongoose Publishing — sous licence pour *Mongoose Traveller 2e*
- Cartes stellaire via [Traveller Map](https://www.travellermap.com)
- Règles de conversion calendaires d'après [Traveller Wiki](https://wiki.travellerrpg.com/Date_Conversion)
- Module initié par **JdR.Ninja**
+12 -4
View File
@@ -723,22 +723,30 @@ export function validateOptions(options = {}) {
const errors = []; const errors = [];
const validated = { ...options }; const validated = { ...options };
if (validated.citizenCategory && !getCitizenCategoryByKey(validated.citizenCategory)) { // Remplir les valeurs manquantes avec les défauts
for (const key of ['citizenCategory', 'experience', 'role', 'gender']) {
if (!validated[key]) {
validated[key] = DEFAULT_OPTIONS[key];
}
}
// Corriger les valeurs invalides (getXByKey renvoie l'objet par défaut si la clé est inconnue)
if (getCitizenCategoryByKey(validated.citizenCategory).key !== validated.citizenCategory) {
errors.push(`Catégorie de citoyen invalide: ${validated.citizenCategory}`); errors.push(`Catégorie de citoyen invalide: ${validated.citizenCategory}`);
validated.citizenCategory = DEFAULT_OPTIONS.citizenCategory; validated.citizenCategory = DEFAULT_OPTIONS.citizenCategory;
} }
if (validated.experience && !getExperienceLevelByKey(validated.experience)) { if (getExperienceLevelByKey(validated.experience).key !== validated.experience) {
errors.push(`Niveau d'expérience invalide: ${validated.experience}`); errors.push(`Niveau d'expérience invalide: ${validated.experience}`);
validated.experience = DEFAULT_OPTIONS.experience; validated.experience = DEFAULT_OPTIONS.experience;
} }
if (validated.role && !getRoleByKey(validated.role)) { if (getRoleByKey(validated.role).key !== validated.role) {
errors.push(`Rôle invalide: ${validated.role}`); errors.push(`Rôle invalide: ${validated.role}`);
validated.role = DEFAULT_OPTIONS.role; validated.role = DEFAULT_OPTIONS.role;
} }
if (validated.gender && !getGenderByKey(validated.gender)) { if (getGenderByKey(validated.gender).key !== validated.gender) {
errors.push(`Genre invalide: ${validated.gender}`); errors.push(`Genre invalide: ${validated.gender}`);
validated.gender = DEFAULT_OPTIONS.gender; validated.gender = DEFAULT_OPTIONS.gender;
} }
+9 -6
View File
@@ -8,6 +8,9 @@
* un framework de test similaire. * un framework de test similaire.
*/ */
// Import explicite des globals Jest (obligatoire en ESM)
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
// Mock des dépendances Foundry pour l'environnement de test // Mock des dépendances Foundry pour l'environnement de test
const mockFoundry = { const mockFoundry = {
utils: { utils: {
@@ -85,7 +88,6 @@ import {
convertSkillToMgt2eFormat, convertSkillToMgt2eFormat,
buildMgt2eCharacteristics, buildMgt2eCharacteristics,
buildMgt2eSkills, buildMgt2eSkills,
buildMgt2eBaseActorSystem,
getMgt2eBaseActorSystem, getMgt2eBaseActorSystem,
toHex, toHex,
calculateDm, calculateDm,
@@ -579,13 +581,14 @@ describe('generateSkills', () => {
expect(eliteLevel3).toBeGreaterThanOrEqual(regularLevel3); expect(eliteLevel3).toBeGreaterThanOrEqual(regularLevel3);
}); });
it('devrait supprimer les doublons basés sur la compétence de base', () => { it('devrait supprimer les doublons exacts de compétence', () => {
const skills = generateSkills('pilot', 'regular'); const skills = generateSkills('pilot', 'regular');
const skillNames = skills.map(s => s.name); const skillNames = skills.map(s => s.name);
const uniqueBases = new Set(skillNames.map(n => n.split('-')[0])); const uniqueNames = new Set(skillNames);
// Chaque nom de base devrait apparaître une seule fois // Le générateur dédoublonne les noms complets mais conserve
expect(skillNames.length).toBe(uniqueBases.size); // les spécialisations (Pilot-Spacecraft, Pilot-Small Craft, ...)
expect(skillNames.length).toBe(uniqueNames.size);
}); });
it('devrait générer des compétences avec le rôle par défaut pour un rôle invalide', () => { it('devrait générer des compétences avec le rôle par défaut pour un rôle invalide', () => {
@@ -624,7 +627,7 @@ describe('convertSkillToMgt2eFormat', () => {
}); });
it('devrait gérer les tirets', () => { it('devrait gérer les tirets', () => {
expect(convertSkillToMgt2eFormat('Pilot-Small Craft')).toBe('pilot.smallcraft'); expect(convertSkillToMgt2eFormat('Pilot-Small Craft')).toBe('pilot.smallCraft');
}); });
}); });
+9 -4
View File
@@ -146,7 +146,7 @@ const SKILL_MAPPING = {
* @param {string} skillName - Nom de la compétence * @param {string} skillName - Nom de la compétence
* @returns {string} - Nom au format mgt2e * @returns {string} - Nom au format mgt2e
*/ */
function convertSkillToMgt2eFormat(skillName) { export function convertSkillToMgt2eFormat(skillName) {
// Vérifier d'abord dans le mapping explicite // Vérifier d'abord dans le mapping explicite
if (SKILL_MAPPING[skillName]) { if (SKILL_MAPPING[skillName]) {
return SKILL_MAPPING[skillName]; return SKILL_MAPPING[skillName];
@@ -192,8 +192,9 @@ export function generateCharacteristics(citizenCategoryKey, roleKey) {
const category = getCitizenCategoryByKey(citizenCategoryKey); const category = getCitizenCategoryByKey(citizenCategoryKey);
const priorities = getCharacteristicPrioritiesForRole(roleKey); const priorities = getCharacteristicPrioritiesForRole(roleKey);
// Cloner et mélanger l'array de base de la catégorie // Le tableau de base de la catégorie est trié par ordre décroissant :
let characteristicArray = shuffleArray([...category.characteristicArray]); // on l'attribue tel quel dans l'ordre de priorité (high → medium → low)
let characteristicArray = [...category.characteristicArray];
const characteristics = {}; const characteristics = {};
@@ -726,5 +727,9 @@ export {
getSkillsForRole, getSkillsForRole,
getCharacteristicPrioritiesForRole, getCharacteristicPrioritiesForRole,
validateOptions, validateOptions,
DEFAULT_OPTIONS DEFAULT_OPTIONS,
popRandomItems
} from './data/travellerNpcGenerator.js'; } from './data/travellerNpcGenerator.js';
// Ré-exporter ERROR_CODES et TravellerNpcError utilisés en interne (proviennent de utils)
export { ERROR_CODES, TravellerNpcError } from './utils/travellerNpcUtils.js';