ef7fe6e2bd
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>
689 lines
21 KiB
JavaScript
689 lines
21 KiB
JavaScript
/**
|
|
* Traveller NPC Generator - Logique métier
|
|
* Basé sur : https://github.com/carloscasalar/traveller-npc-generator
|
|
*
|
|
* Ce fichier contient la logique de génération des PNJ Traveller.
|
|
*/
|
|
|
|
import {
|
|
CITIZEN_CATEGORY,
|
|
EXPERIENCE_LEVEL,
|
|
ROLE,
|
|
ROLE_SKILLS,
|
|
CHARACTERISTIC_PRIORITIES,
|
|
GENDER,
|
|
CHARACTERISTIC,
|
|
CHARACTERISTIC_LIST,
|
|
UPP_ORDER,
|
|
toHex,
|
|
calculateDm,
|
|
pickRandomItem,
|
|
shuffleArray,
|
|
getRoleByKey,
|
|
getCitizenCategoryByKey,
|
|
getExperienceLevelByKey,
|
|
getGenderByKey,
|
|
getSkillsForRole,
|
|
getCharacteristicPrioritiesForRole,
|
|
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';
|
|
|
|
// ============================================================================
|
|
// 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
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Génère les caractéristiques d'un PNJ en fonction de sa catégorie et de son rôle
|
|
*
|
|
* @param {string} citizenCategoryKey - Clé de la catégorie de citoyen
|
|
* @param {string} roleKey - Clé du rôle
|
|
* @returns {Object} - Objet contenant les caractéristiques et l'UPP
|
|
*/
|
|
export function generateCharacteristics(citizenCategoryKey, roleKey) {
|
|
const category = getCitizenCategoryByKey(citizenCategoryKey);
|
|
const priorities = getCharacteristicPrioritiesForRole(roleKey);
|
|
|
|
// Cloner et mélanger l'array de base de la catégorie
|
|
let characteristicArray = shuffleArray([...category.characteristicArray]);
|
|
|
|
const characteristics = {};
|
|
|
|
// 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 }
|
|
];
|
|
|
|
// 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;
|
|
});
|
|
}
|
|
|
|
// 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('');
|
|
|
|
return {
|
|
characteristics,
|
|
upp,
|
|
category
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Génération des compétences
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 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
|
|
* @returns {Array<{name: string, level: number}>} - Compétences avec niveaux
|
|
*/
|
|
function distributeSkillLevels(roleSkills, distribution) {
|
|
// 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 seen = new Set();
|
|
|
|
for (const skill of roleSkills) {
|
|
if (!seen.has(skill)) {
|
|
seen.add(skill);
|
|
uniqueSkills.push(skill);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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
|
|
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 (déjà initialisé)
|
|
|
|
// 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));
|
|
}
|
|
|
|
/**
|
|
* Génère les compétences d'un PNJ en fonction de son rôle et de son expérience
|
|
*
|
|
* @param {string} roleKey - Clé du rôle
|
|
* @param {string} experienceKey - Clé du niveau d'expérience
|
|
* @returns {Array<{name: string, level: number}>} - Liste des compétences avec niveaux
|
|
*/
|
|
export function generateSkills(roleKey, experienceKey) {
|
|
const roleSkills = getSkillsForRole(roleKey);
|
|
const experience = getExperienceLevelByKey(experienceKey);
|
|
const distribution = experience.skillDistribution;
|
|
|
|
return distributeSkillLevels(roleSkills, distribution);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Génération complète du PNJ
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Génère un PNJ Traveller complet
|
|
*
|
|
* @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 = {}) {
|
|
// Valider et fusionner avec les options par défaut
|
|
const opts = validateOptions({
|
|
...DEFAULT_OPTIONS,
|
|
...options
|
|
});
|
|
|
|
// Générer le nom
|
|
let name;
|
|
if (opts.firstName && opts.surname) {
|
|
name = {
|
|
firstName: opts.firstName,
|
|
surname: opts.surname,
|
|
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,
|
|
surname,
|
|
fullName: `${firstName} ${surname}`
|
|
};
|
|
}
|
|
|
|
// Générer les caractéristiques
|
|
const { characteristics, upp, category } = generateCharacteristics(
|
|
opts.citizenCategory,
|
|
opts.role
|
|
);
|
|
|
|
// Générer les compétences
|
|
const skills = generateSkills(opts.role, opts.experience);
|
|
|
|
// Convertir les compétences au format mgt2e pour la création de fiche
|
|
const skillsForActor = skills.map(s => ({
|
|
name: convertSkillToMgt2eFormat(s.name),
|
|
level: s.level
|
|
}));
|
|
|
|
// Récupérer les objets complets pour les références
|
|
const citizenCategory = getCitizenCategoryByKey(opts.citizenCategory);
|
|
const experience = getExperienceLevelByKey(opts.experience);
|
|
const role = getRoleByKey(opts.role);
|
|
const gender = getGenderByKey(opts.gender);
|
|
|
|
// Libellés des caractéristiques pour l'affichage (en français)
|
|
const characteristicLabels = {};
|
|
CHARACTERISTIC_LIST.forEach(char => {
|
|
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',
|
|
name,
|
|
role,
|
|
citizenCategory,
|
|
experience,
|
|
gender,
|
|
characteristics,
|
|
upp,
|
|
skills: skillsWithLabels,
|
|
skillsForActor,
|
|
MODULE_ID,
|
|
UPP_ORDER,
|
|
display: {
|
|
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
|
|
}
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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 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 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @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 char of CHARACTERISTIC_LIST) {
|
|
const value = characteristics[char.key] || 7;
|
|
result[char.mgt2eKey] = foundry.utils.mergeObject(result[char.mgt2eKey] ?? {}, {
|
|
value,
|
|
current: value,
|
|
dm: calculateDm(value),
|
|
show: true,
|
|
default: false,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Construit les compétences au format mgt2e
|
|
*
|
|
* @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, alreadyMapped = false) {
|
|
const result = foundry.utils.deepClone(existingSkills);
|
|
|
|
for (const { name, level } of skills) {
|
|
const skillName = alreadyMapped ? name : convertSkillToMgt2eFormat(name);
|
|
setSkillLevel(result, skillName, level);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Construit la description de l'acteur
|
|
*
|
|
* @param {Object} npcData - Données du PNJ généré
|
|
* @param {string} actorName - Nom de l'acteur
|
|
* @returns {string} - Description formatée
|
|
*/
|
|
function buildActorDescription(npcData, actorName) {
|
|
const notableSkills = npcData.skills
|
|
.filter(s => s.level > 0)
|
|
.map(s => {
|
|
try {
|
|
return localizeSkill(s.name) || s.name;
|
|
} catch (e) {
|
|
return s.name;
|
|
}
|
|
})
|
|
.join(', ');
|
|
|
|
const lines = [
|
|
`${actorName} — ${npcData.role.label}`,
|
|
`Catégorie : ${npcData.citizenCategory.label}`,
|
|
`Expérience : ${npcData.experience.label}`,
|
|
`UPP : ${npcData.upp}`,
|
|
`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
|
|
*
|
|
* @param {Object} npcData - Données du PNJ généré
|
|
* @param {Object} options - Options de création
|
|
* @param {string} [options.name] - Nom de l'acteur
|
|
* @param {boolean} [options.openSheet=true] - Ouvrir la fiche après création
|
|
* @returns {Promise<Actor|null>} - Acteur créé ou null
|
|
*/
|
|
export async function createTravellerNpcActor(npcData, options = {}) {
|
|
try {
|
|
const requestedName = options.name?.trim();
|
|
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',
|
|
img: 'systems/mgt2e/icons/cargo/passenger-middle.svg',
|
|
system: {
|
|
settings: foundry.utils.mergeObject(
|
|
foundry.utils.deepClone(baseActorSystem?.settings ?? {}),
|
|
{
|
|
hideUntrained: true,
|
|
lockCharacteristics: true,
|
|
}
|
|
),
|
|
sophont: foundry.utils.mergeObject(
|
|
foundry.utils.deepClone(baseActorSystem?.sophont ?? {}),
|
|
{
|
|
age: 18 + Math.floor(Math.random() * 40),
|
|
homeworld: '',
|
|
profession: npcData.role.label,
|
|
}
|
|
),
|
|
characteristics: buildMgt2eCharacteristics(
|
|
foundry.utils.deepClone(baseActorSystem?.characteristics ?? {}),
|
|
npcData.characteristics
|
|
),
|
|
hits: foundry.utils.deepClone(baseActorSystem?.hits ?? {}),
|
|
skills: buildMgt2eSkills(
|
|
foundry.utils.deepClone(baseActorSystem?.skills ?? {}),
|
|
npcData.skillsForActor,
|
|
true
|
|
),
|
|
description,
|
|
},
|
|
flags: {
|
|
[MODULE_ID]: {
|
|
generatedTravellerNpc: {
|
|
version: 1,
|
|
role: npcData.role.key,
|
|
citizenCategory: npcData.citizenCategory.key,
|
|
experience: npcData.experience.key,
|
|
gender: npcData.gender.key,
|
|
upp: npcData.upp,
|
|
generatedAt: new Date().toISOString()
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const actor = await Actor.create(actorData, { renderSheet: false });
|
|
|
|
if (options.openSheet !== false) {
|
|
actor.sheet?.render(true);
|
|
}
|
|
|
|
return actor;
|
|
} catch (error) {
|
|
const npcError = TravellerNpcError.from(error, ERROR_CODES.ACTOR_CREATION_FAILED);
|
|
npcError.notify();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Fonction principale
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Fonction principale pour générer un PNJ Traveller
|
|
* Peut créer une fiche d'acteur si demandé
|
|
*
|
|
* @param {TravellerNpcOptions} [options={}]
|
|
* @returns {Promise<TravellerNpcResult>}
|
|
*/
|
|
export async function generateAndCreateTravellerNpc(options = {}) {
|
|
const npcData = generateTravellerNpc(options);
|
|
|
|
if (options.createActor) {
|
|
const actor = await createTravellerNpcActor(npcData, {
|
|
name: options.actorName,
|
|
openSheet: options.openCreatedActor !== false
|
|
});
|
|
|
|
if (actor) {
|
|
npcData.createdActor = {
|
|
id: actor.id,
|
|
name: actor.name
|
|
};
|
|
}
|
|
}
|
|
|
|
return npcData;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Export des types et données pour compatibilité
|
|
// ============================================================================
|
|
|
|
// Ré-exporter les données pour facilitier les imports
|
|
export {
|
|
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,
|
|
toHex,
|
|
calculateDm,
|
|
pickRandomItem,
|
|
shuffleArray,
|
|
getRoleByKey,
|
|
getCitizenCategoryByKey,
|
|
getExperienceLevelByKey,
|
|
getGenderByKey,
|
|
getSkillsForRole,
|
|
getCharacteristicPrioritiesForRole,
|
|
validateOptions,
|
|
DEFAULT_OPTIONS
|
|
} from './data/travellerNpcGenerator.js';
|