Corrections et améliorations pour /gennpc - v1.3.0
Corrections critiques implémentées: - Remplacement du cache global mutable par ModuleCache - Binding des méthodes dans TravellerNpcDialog - Suppression des ré-exports circulaires - Validation complète des options - Correction: Duplicate export de TravellerNpcDialog et openTravellerNpcDialog - Correction: distributeSkillLevels ne supprime plus les spécialisations (ex: Pilot-Spacecraft ET Pilot-Small Craft sont maintenant conservées) Améliorations majeures: - Optimisation de l'algorithme de distribution des compétences (single-pass) - Optimisation de la génération des caractéristiques (priorité-based) - Gestion d'erreur améliorée avec TravellerNpcError - Création de TravellerNpcUtils.js avec classes utilitaires Améliorations mineures: - CSS aligné avec les styles des dialogues /commerce et /pnj - Thème clair cohérent (#f5f0e8 background, #222 text) - Fieldset, onglets, formulaires alignés sur mgt2-npc-form - Boutons et résultats stylisés comme mgt2-npc-result - Suppression des styles inline redondants dans _applyThemeStyles - Design réactif, accessibilité, impression - Tests unitaires complets pour toutes les fonctions - Version bumpée à 1.3.0 Traductions en français: - Ajout de SKILL_LABELS_FR pour toutes les compétences Traveller - Ajout de CHARACTERISTIC_LABELS_FR pour STR, DEX, END, INT, EDU, SOC - Ajout de CITIZEN_CATEGORY_LABELS_FR, EXPERIENCE_LEVEL_LABELS_FR - Ajout de ROLE_LABELS_FR, GENDER_LABELS_FR - Mise à jour de generateTravellerNpc pour utiliser les libellés français - Mise à jour du template traveller-npc-result.hbs pour afficher labelFr - Mise à jour du template traveller-npc-dialog.hbs avec libellés français - Mise à jour de TravellerNpcDialog._prepareContext pour utiliser les libellés FR Fichiers ajoutés: - scripts/utils/travellerNpcUtils.js - scripts/tests/travellerNpcGenerator.test.js Fichiers modifiés: - scripts/data/travellerNpcGenerator.js (+ traductions FR) - scripts/travellerNpcGenerator.js (+ fonctions getSkillLabelFr, getCharacteristicLabelFr) - scripts/TravellerNpcDialog.js (libellés FR dans _prepareContext) - scripts/npc.js - styles/traveller-npc.css - templates/traveller-npc-dialog.hbs - templates/traveller-npc-result.hbs - module.json Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
+329
-304
@@ -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<string, string>}
|
||||
*/
|
||||
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<string, number>} - 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<Object|null>} - Système de base ou null
|
||||
*/
|
||||
async function getMgt2eBaseActorSystem() {
|
||||
if (!mgt2eBaseActorSystemPromise) {
|
||||
mgt2eBaseActorSystemPromise = (async () => {
|
||||
try {
|
||||
const pack = game.packs.get('mgt2e.base-actors');
|
||||
if (!pack) return null;
|
||||
|
||||
const index = Array.from(await pack.getIndex({ fields: ['name', 'type'] }));
|
||||
const entry = index.find((document) => document.name === 'DEFAULT TRAVELLER')
|
||||
?? index.find((document) => document.type === 'traveller')
|
||||
?? index[0];
|
||||
if (!entry?._id) return null;
|
||||
|
||||
const document = await pack.getDocument(entry._id);
|
||||
return document?.toObject()?.system ?? null;
|
||||
} catch (error) {
|
||||
console.warn(`${MODULE_ID} | Erreur lors de la récupération du système de base mgt2e:`, error);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
async function fetchMgt2eBaseActorSystem() {
|
||||
try {
|
||||
// Vérifier que le système mgt2e est actif
|
||||
if (game.system?.id !== 'mgt2e') {
|
||||
console.warn(`${MODULE_ID} | Le système mgt2e n'est pas actif`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const pack = game.packs.get('mgt2e.base-actors');
|
||||
if (!pack) {
|
||||
console.warn(`${MODULE_ID} | Le compendium mgt2e.base-actors n'est pas disponible`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = Array.from(await pack.getIndex({ fields: ['name', 'type'] }));
|
||||
const entry = index.find((document) => document.name === 'DEFAULT TRAVELLER')
|
||||
?? index.find((document) => document.type === 'traveller')
|
||||
?? index[0];
|
||||
|
||||
if (!entry?._id) {
|
||||
console.warn(`${MODULE_ID} | Aucun acteur de base trouvé dans mgt2e.base-actors`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = await pack.getDocument(entry._id);
|
||||
const system = document?.toObject()?.system;
|
||||
|
||||
if (!system) {
|
||||
console.warn(`${MODULE_ID} | Le système de l'acteur de base est vide`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return system;
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Erreur lors de la récupération du système de base mgt2e:`, error);
|
||||
throw new TravellerNpcError(
|
||||
'Erreur lors de la récupération du système de base mgt2e',
|
||||
ERROR_CODES.BASE_ACTOR_NOT_FOUND,
|
||||
{ error: error.message }
|
||||
);
|
||||
}
|
||||
|
||||
const system = await mgt2eBaseActorSystemPromise;
|
||||
return system ? foundry.utils.deepClone(system) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le système de base des acteurs mgt2e avec cache
|
||||
* @param {Object} [options={}] - Options
|
||||
* @param {boolean} [options.forceRefresh=false] - Forcer le rafraîchissement
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
export async function getMgt2eBaseActorSystem(options = {}) {
|
||||
const { forceRefresh = false } = options;
|
||||
const cacheKey = 'baseActorSystem';
|
||||
|
||||
return travellerNpcCache.getOrFetch(
|
||||
cacheKey,
|
||||
fetchMgt2eBaseActorSystem,
|
||||
{ forceRefresh }
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Construction des données pour l'acteur
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Construit les caractéristiques au format mgt2e
|
||||
*
|
||||
* @param {Object} existingCharacteristics - Caractéristiques existantes (optionnel)
|
||||
* @param {Object} existingCharacteristics - Caractéristiques existantes
|
||||
* @param {Object} characteristics - Caractéristiques générées
|
||||
* @returns {Object} - Caractéristiques au format mgt2e
|
||||
*/
|
||||
export function buildMgt2eCharacteristics(existingCharacteristics = {}, characteristics) {
|
||||
const result = foundry.utils.deepClone(existingCharacteristics);
|
||||
|
||||
for (const [key, char] of Object.entries(CHARACTERISTIC)) {
|
||||
for (const char of CHARACTERISTIC_LIST) {
|
||||
const value = characteristics[char.key] || 7;
|
||||
result[char.mgt2eKey] = foundry.utils.mergeObject(result[char.mgt2eKey] ?? {}, {
|
||||
value,
|
||||
@@ -462,17 +480,16 @@ export function buildMgt2eCharacteristics(existingCharacteristics = {}, characte
|
||||
/**
|
||||
* Construit les compétences au format mgt2e
|
||||
*
|
||||
* @param {Object} existingSkills - Compétences existantes (optionnel)
|
||||
* @param {Object} existingSkills - Compétences existantes
|
||||
* @param {Array<{name: string, level: number}>} skills - Compétences générées
|
||||
* @param {boolean} [alreadyMapped=false] - Les compétences sont déjà au format mgt2e
|
||||
* @returns {Object} - Compétences au format mgt2e
|
||||
*/
|
||||
export function buildMgt2eSkills(existingSkills = {}, skills, useMgt2eFormat = false) {
|
||||
export function buildMgt2eSkills(existingSkills = {}, skills, alreadyMapped = false) {
|
||||
const result = foundry.utils.deepClone(existingSkills);
|
||||
|
||||
for (const { name, level } of skills) {
|
||||
// Si useMgt2eFormat est vrai, on utilise directement le nom
|
||||
// Sinon, on convertit
|
||||
const skillName = useMgt2eFormat ? name : convertSkillToMgt2eFormat(name);
|
||||
const skillName = alreadyMapped ? name : convertSkillToMgt2eFormat(name);
|
||||
setSkillLevel(result, skillName, level);
|
||||
}
|
||||
|
||||
@@ -490,25 +507,33 @@ function buildActorDescription(npcData, actorName) {
|
||||
const notableSkills = npcData.skills
|
||||
.filter(s => s.level > 0)
|
||||
.map(s => {
|
||||
// Essayer de localiser la compétence
|
||||
try {
|
||||
return localizeSkill(s.name);
|
||||
return localizeSkill(s.name) || s.name;
|
||||
} catch (e) {
|
||||
return s.name;
|
||||
}
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
return [
|
||||
const lines = [
|
||||
`${actorName} — ${npcData.role.label}`,
|
||||
`Catégorie : ${npcData.citizenCategory.label}`,
|
||||
`Expérience : ${npcData.experience.label}`,
|
||||
`UPP : ${npcData.upp}`,
|
||||
`Genre : ${npcData.gender.label}`,
|
||||
notableSkills ? `Compétences : ${notableSkills}` : ''
|
||||
].filter(line => line).join('\n');
|
||||
`Genre : ${npcData.gender.label}`
|
||||
];
|
||||
|
||||
if (notableSkills) {
|
||||
lines.push(`Compétences : ${notableSkills}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Création de la fiche d'acteur
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Crée une fiche d'acteur pour un PNJ Traveller
|
||||
*
|
||||
@@ -521,10 +546,22 @@ function buildActorDescription(npcData, actorName) {
|
||||
export async function createTravellerNpcActor(npcData, options = {}) {
|
||||
try {
|
||||
const requestedName = options.name?.trim();
|
||||
const baseActorSystem = game.system?.id === 'mgt2e' ? await getMgt2eBaseActorSystem() : null;
|
||||
|
||||
const actorName = requestedName || npcData.name.fullName || `PNJ — ${npcData.role.label}`;
|
||||
|
||||
// Vérifier que mgt2e est actif
|
||||
if (game.system?.id !== 'mgt2e') {
|
||||
throw new TravellerNpcError(
|
||||
'Le système mgt2e doit être actif pour créer des fiches PNJ Traveller',
|
||||
ERROR_CODES.MGT2E_NOT_ACTIVE
|
||||
);
|
||||
}
|
||||
|
||||
// Récupérer le système de base
|
||||
const baseActorSystem = await getMgt2eBaseActorSystem();
|
||||
|
||||
// Construire la description (avec <br> pour HTML)
|
||||
const description = buildActorDescription(npcData, actorName).replace(/\n/g, '<br>');
|
||||
|
||||
const actorData = {
|
||||
name: actorName,
|
||||
type: 'npc',
|
||||
@@ -540,7 +577,7 @@ export async function createTravellerNpcActor(npcData, options = {}) {
|
||||
sophont: foundry.utils.mergeObject(
|
||||
foundry.utils.deepClone(baseActorSystem?.sophont ?? {}),
|
||||
{
|
||||
age: 18 + Math.floor(Math.random() * 40), // Âge entre 18 et 58 ans
|
||||
age: 18 + Math.floor(Math.random() * 40),
|
||||
homeworld: '',
|
||||
profession: npcData.role.label,
|
||||
}
|
||||
@@ -553,9 +590,9 @@ export async function createTravellerNpcActor(npcData, options = {}) {
|
||||
skills: buildMgt2eSkills(
|
||||
foundry.utils.deepClone(baseActorSystem?.skills ?? {}),
|
||||
npcData.skillsForActor,
|
||||
true // Utiliser le format mgt2e directement
|
||||
true
|
||||
),
|
||||
description: buildActorDescription(npcData, actorName),
|
||||
description,
|
||||
},
|
||||
flags: {
|
||||
[MODULE_ID]: {
|
||||
@@ -572,18 +609,6 @@ export async function createTravellerNpcActor(npcData, options = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
// S'assurer que le nom est défini
|
||||
actorData.name = actorName;
|
||||
actorData.system.sophont = foundry.utils.mergeObject(
|
||||
actorData.system.sophont ?? {},
|
||||
{
|
||||
profession: npcData.role.label,
|
||||
}
|
||||
);
|
||||
|
||||
// Remplacer les sauts de ligne par des <br> pour HTML
|
||||
actorData.system.description = actorData.system.description.replace(/\n/g, '<br>');
|
||||
|
||||
const actor = await Actor.create(actorData, { renderSheet: false });
|
||||
|
||||
if (options.openSheet !== false) {
|
||||
@@ -592,36 +617,28 @@ export async function createTravellerNpcActor(npcData, options = {}) {
|
||||
|
||||
return actor;
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Erreur lors de la création de l'acteur Traveller NPC:`, error);
|
||||
ui.notifications.error(`Erreur lors de la création de la fiche PNJ: ${error.message}`);
|
||||
const npcError = TravellerNpcError.from(error, ERROR_CODES.ACTOR_CREATION_FAILED);
|
||||
npcError.notify();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fonction principale exportée
|
||||
// Fonction principale
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fonction principale pour générer un PNJ Traveller
|
||||
* Peut créer une fiche d'acteur si demandé
|
||||
*
|
||||
* @param {Object} options - Options de génération
|
||||
* @param {string} [options.citizenCategory] - Catégorie de citoyen
|
||||
* @param {string} [options.experience] - Niveau d'expérience
|
||||
* @param {string} [options.role] - Rôle
|
||||
* @param {string} [options.gender] - Genre
|
||||
* @param {boolean} [options.createActor] - Créer une fiche d'acteur
|
||||
* @param {string} [options.actorName] - Nom de la fiche
|
||||
* @param {boolean} [options.openCreatedActor] - Ouvrir la fiche créée
|
||||
* @returns {Promise<Object>} - Résultat avec le PNJ généré et éventuellement l'acteur
|
||||
* @param {TravellerNpcOptions} [options={}]
|
||||
* @returns {Promise<TravellerNpcResult>}
|
||||
*/
|
||||
export async function generateAndCreateTravellerNpc(options = {}) {
|
||||
const npcData = generateTravellerNpc(options);
|
||||
|
||||
let actor = null;
|
||||
if (options.createActor) {
|
||||
actor = await createTravellerNpcActor(npcData, {
|
||||
const actor = await createTravellerNpcActor(npcData, {
|
||||
name: options.actorName,
|
||||
openSheet: options.openCreatedActor !== false
|
||||
});
|
||||
@@ -638,26 +655,34 @@ export async function generateAndCreateTravellerNpc(options = {}) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export des fonctions utilitaires pour les tests
|
||||
// Export des types et données pour compatibilité
|
||||
// ============================================================================
|
||||
|
||||
// Ré-exporter les données pour facilitier les imports
|
||||
export {
|
||||
generateCharacteristics,
|
||||
generateSkills,
|
||||
generateName,
|
||||
buildMgt2eCharacteristics,
|
||||
buildMgt2eSkills,
|
||||
// Ré-exporter les données
|
||||
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,
|
||||
shuffleArray,
|
||||
getRoleByKey,
|
||||
getCitizenCategoryByKey,
|
||||
getExperienceLevelByKey,
|
||||
getGenderByKey,
|
||||
getSkillsForRole,
|
||||
getCharacteristicPrioritiesForRole,
|
||||
validateOptions,
|
||||
DEFAULT_OPTIONS
|
||||
} from './data/travellerNpcGenerator.js';
|
||||
|
||||
Reference in New Issue
Block a user