Ajout de la commande /gennpc pour générer des PNJ Traveller
Implémentation complète du générateur de PNJ Traveller basé sur : https://github.com/carloscasalar/traveller-npc-generator Fonctionnalités : - Génération de caractéristiques selon 4 catégories de citoyens - Distribution des compétences selon 6 niveaux d'expérience - 14 rôles différents avec priorités de caractéristiques spécifiques - Génération de noms aléatoires (masculin/féminin/neutre) - Création de fiche d'acteur mgt2e avec toutes les compétences - Interface utilisateur avec dialogue Handlebars - Commande /gennpc dans le chat Fichiers ajoutés : - scripts/data/travellerNpcGenerator.js (données et constantes) - scripts/travellerNpcGenerator.js (logique métier) - scripts/TravellerNpcDialog.js (interface utilisateur) - templates/traveller-npc-dialog.hbs (template dialogue) - templates/traveller-npc-result.hbs (template résultat) - styles/traveller-npc.css (styles spécifiques) Fichiers modifiés : - scripts/npc.js (intégration de la commande) - module.json (ajout des nouveaux scripts et styles) Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Traveller NPC Generator - Dialogue de génération
|
||||
*
|
||||
* Ce fichier contient le dialogue pour générer des PNJ Traveller.
|
||||
*/
|
||||
|
||||
import { generateAndCreateTravellerNpc } from './travellerNpcGenerator.js';
|
||||
import {
|
||||
CITIZEN_CATEGORY_LIST,
|
||||
EXPERIENCE_LEVEL_LIST,
|
||||
ROLE_LIST,
|
||||
GENDER_LIST,
|
||||
DEFAULT_OPTIONS,
|
||||
CHARACTERISTIC_LIST,
|
||||
UPP_ORDER
|
||||
} from './data/travellerNpcGenerator.js';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: 'mgt2-traveller-npc',
|
||||
classes: ['mgt2-npc-dialog', 'mgt2-traveller-npc-dialog'],
|
||||
position: {
|
||||
width: 700,
|
||||
height: 'auto',
|
||||
},
|
||||
window: {
|
||||
title: 'Générateur de PNJ Traveller – MgT2e',
|
||||
resizable: true,
|
||||
},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: `modules/${MODULE_ID}/templates/traveller-npc-dialog.hbs`,
|
||||
root: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
|
||||
// Form data avec valeurs par défaut
|
||||
this._formData = {
|
||||
citizenCategory: options.citizenCategory || DEFAULT_OPTIONS.citizenCategory,
|
||||
experience: options.experience || DEFAULT_OPTIONS.experience,
|
||||
role: options.role || DEFAULT_OPTIONS.role,
|
||||
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
|
||||
createActor: options.createActor !== undefined ? options.createActor : DEFAULT_OPTIONS.createActor,
|
||||
actorName: options.actorName || '',
|
||||
openCreatedActor: options.openCreatedActor !== undefined ? options.openCreatedActor : DEFAULT_OPTIONS.openCreatedActor,
|
||||
};
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
registerHandlebarsHelpers();
|
||||
return {
|
||||
...this._formData,
|
||||
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
|
||||
key: c.key,
|
||||
label: c.label,
|
||||
description: c.description
|
||||
})),
|
||||
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
|
||||
key: e.key,
|
||||
label: e.label,
|
||||
description: e.description
|
||||
})),
|
||||
roles: ROLE_LIST.map(r => ({
|
||||
key: r.key,
|
||||
label: r.label,
|
||||
description: r.description
|
||||
})),
|
||||
genders: GENDER_LIST.map(g => ({
|
||||
key: g.key,
|
||||
label: g.label
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
const html = this._getForm();
|
||||
if (!html?.length) return;
|
||||
|
||||
html.addClass('mgt2-traveller-npc-form');
|
||||
this._applyThemeStyles(html);
|
||||
|
||||
// Gestion des événements
|
||||
html.find('[data-action="generate-traveller-npc"]').on('click', async (event) => {
|
||||
event.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handleGenerate();
|
||||
});
|
||||
|
||||
html.find('[data-action="randomize-name"]').on('click', (event) => {
|
||||
event.preventDefault();
|
||||
this._randomizeName(html);
|
||||
});
|
||||
|
||||
// Gestion du basculement entre nom aléatoire et nom personnalisé
|
||||
html.find('[name="useRandomName"]').on('change', (event) => {
|
||||
const useRandom = event.target.checked;
|
||||
html.find('.name-fields').toggleClass('hidden', useRandom);
|
||||
});
|
||||
|
||||
// Initialiser l'affichage des champs de nom
|
||||
html.find('.name-fields').toggleClass('hidden', this._formData.useRandomName);
|
||||
}
|
||||
|
||||
_getForm() {
|
||||
return $(this.element).find('.window-content');
|
||||
}
|
||||
|
||||
_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'
|
||||
});
|
||||
}
|
||||
|
||||
_readForm(html) {
|
||||
this._formData.citizenCategory = html.find('[name="citizenCategory"]').val();
|
||||
this._formData.experience = html.find('[name="experience"]').val();
|
||||
this._formData.role = html.find('[name="role"]').val();
|
||||
this._formData.gender = html.find('[name="gender"]').val();
|
||||
this._formData.firstName = html.find('[name="firstName"]').val();
|
||||
this._formData.surname = html.find('[name="surname"]').val();
|
||||
this._formData.useRandomName = html.find('[name="useRandomName"]').is(':checked');
|
||||
this._formData.createActor = html.find('[name="createActor"]').is(':checked');
|
||||
this._formData.actorName = html.find('[name="actorName"]').val();
|
||||
this._formData.openCreatedActor = html.find('[name="openCreatedActor"]').is(':checked');
|
||||
}
|
||||
|
||||
_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');
|
||||
});
|
||||
}
|
||||
|
||||
async _handleGenerate() {
|
||||
const button = $(this.element).find('[data-action="generate-traveller-npc"]');
|
||||
const originalLabel = button.html();
|
||||
|
||||
try {
|
||||
// Désactiver le bouton pendant la génération
|
||||
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Génération...');
|
||||
|
||||
// Préparer les options de génération
|
||||
const generateOptions = {
|
||||
citizenCategory: this._formData.citizenCategory,
|
||||
experience: this._formData.experience,
|
||||
role: this._formData.role,
|
||||
gender: this._formData.gender,
|
||||
createActor: this._formData.createActor,
|
||||
actorName: this._formData.actorName,
|
||||
openCreatedActor: this._formData.openCreatedActor
|
||||
};
|
||||
|
||||
// Si on n'utilise pas de nom aléatoire, passer le nom personnalisé
|
||||
if (!this._formData.useRandomName && this._formData.firstName && this._formData.surname) {
|
||||
generateOptions.firstName = this._formData.firstName;
|
||||
generateOptions.surname = this._formData.surname;
|
||||
}
|
||||
|
||||
// Générer le PNJ
|
||||
const result = await generateAndCreateTravellerNpc(generateOptions);
|
||||
|
||||
if (result.success) {
|
||||
// Afficher le résultat dans le chat
|
||||
await this._postToChatResult(result);
|
||||
|
||||
if (result.createdActor) {
|
||||
ui.notifications.info(`Fiche PNJ Traveller créée : ${result.createdActor.name}`);
|
||||
}
|
||||
} else {
|
||||
ui.notifications.error('Erreur lors de la génération du PNJ Traveller');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Erreur lors de la génération du PNJ Traveller:`, error);
|
||||
ui.notifications.error(`Erreur: ${error.message}`);
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
button.prop('disabled', false).html(originalLabel);
|
||||
}
|
||||
}
|
||||
|
||||
async _postToChatResult(data) {
|
||||
registerHandlebarsHelpers();
|
||||
const html = await foundry.applications.handlebars.renderTemplate(
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
||||
data
|
||||
);
|
||||
|
||||
await ChatMessage.create({
|
||||
content: html,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
flags: { [MODULE_ID]: { type: 'traveller-npc-result' } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
let helpersRegistered = false;
|
||||
|
||||
function registerHandlebarsHelpers() {
|
||||
if (helpersRegistered) return;
|
||||
helpersRegistered = true;
|
||||
|
||||
// Helper pour comparer deux valeurs
|
||||
Handlebars.registerHelper('eq', (a, b) => a === b);
|
||||
|
||||
// Helper pour rejoindre un tableau
|
||||
Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : ''));
|
||||
|
||||
// Helper pour vérifier si une valeur contient du texte
|
||||
Handlebars.registerHelper('contains', (text, search) => String(text ?? '').includes(search));
|
||||
|
||||
// Helper pour vérifier si a > b
|
||||
Handlebars.registerHelper('gt', (a, b) => a > b);
|
||||
|
||||
// Helper pour afficher le niveau de compétence avec un symbole
|
||||
Handlebars.registerHelper('skillLevelSymbol', (level) => {
|
||||
if (level === 0) return '';
|
||||
if (level === 1) return '★';
|
||||
if (level === 2) return '★★';
|
||||
if (level === 3) return '★★★';
|
||||
return `+${level}`;
|
||||
});
|
||||
|
||||
// Helper pour formater le DM
|
||||
Handlebars.registerHelper('formatDm', (value) => {
|
||||
const dm = Math.floor((value - 6) / 3);
|
||||
return dm >= 0 ? `+${dm}` : `${dm}`;
|
||||
});
|
||||
|
||||
// Helper pour obtenir la classe CSS du niveau de compétence
|
||||
Handlebars.registerHelper('skillLevelClass', (level) => {
|
||||
if (level === 3) return 'skill-level-3';
|
||||
if (level === 2) return 'skill-level-2';
|
||||
if (level === 1) return 'skill-level-1';
|
||||
return 'skill-level-0';
|
||||
});
|
||||
|
||||
// Helper pour 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 '';
|
||||
return obj[key] !== undefined ? obj[key] : '';
|
||||
});
|
||||
}
|
||||
|
||||
// Exporter pour pouvoir l'ouvrir depuis d'autres modules
|
||||
export function openTravellerNpcDialog(options = {}) {
|
||||
new TravellerNpcDialog(options).render({ force: true });
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
/**
|
||||
* Traveller NPC Generator - Données de configuration
|
||||
* Basé sur : https://github.com/carloscasalar/traveller-npc-generator
|
||||
*
|
||||
* Ce fichier contient toutes les données nécessaires pour générer des PNJ
|
||||
* selon les règles du générateur Traveller.
|
||||
*/
|
||||
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
// ============================================================================
|
||||
// Catégories de citoyens
|
||||
// ============================================================================
|
||||
|
||||
export const CITIZEN_CATEGORY = {
|
||||
BELOW_AVERAGE: {
|
||||
key: 'belowAverage',
|
||||
label: 'En dessous de la moyenne',
|
||||
value: 0,
|
||||
characteristicArray: [8, 7, 6, 6, 5, 4],
|
||||
description: 'Citoyen avec des capacités inférieures à la moyenne'
|
||||
},
|
||||
AVERAGE: {
|
||||
key: 'average',
|
||||
label: 'Moyenne',
|
||||
value: 1,
|
||||
characteristicArray: [9, 8, 7, 7, 6, 5],
|
||||
description: 'Citoyen moyen'
|
||||
},
|
||||
ABOVE_AVERAGE: {
|
||||
key: 'aboveAverage',
|
||||
label: 'Au-dessus de la moyenne',
|
||||
value: 2,
|
||||
characteristicArray: [10, 9, 8, 8, 7, 6],
|
||||
description: 'Citoyen avec des capacités supérieures à la moyenne'
|
||||
},
|
||||
EXCEPTIONAL: {
|
||||
key: 'exceptional',
|
||||
label: 'Exceptionnel',
|
||||
value: 3,
|
||||
characteristicArray: [11, 10, 9, 9, 8, 7],
|
||||
description: 'Citoyen exceptionnel'
|
||||
}
|
||||
};
|
||||
|
||||
export const CITIZEN_CATEGORY_LIST = [
|
||||
CITIZEN_CATEGORY.BELOW_AVERAGE,
|
||||
CITIZEN_CATEGORY.AVERAGE,
|
||||
CITIZEN_CATEGORY.ABOVE_AVERAGE,
|
||||
CITIZEN_CATEGORY.EXCEPTIONAL
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Niveaux d'expérience
|
||||
// ============================================================================
|
||||
|
||||
export const EXPERIENCE_LEVEL = {
|
||||
RECRUIT: {
|
||||
key: 'recruit',
|
||||
label: 'Recrue',
|
||||
value: 0,
|
||||
skillDistribution: {
|
||||
level0: 4,
|
||||
level1: 0,
|
||||
level2: 0,
|
||||
level3: 0
|
||||
},
|
||||
description: 'Nouveau, sans expérience'
|
||||
},
|
||||
ROOKIE: {
|
||||
key: 'rookie',
|
||||
label: 'Débutant',
|
||||
value: 1,
|
||||
skillDistribution: {
|
||||
level0: 4,
|
||||
level1: 2,
|
||||
level2: 0,
|
||||
level3: 0
|
||||
},
|
||||
description: 'Débutant avec un peu d\'expérience'
|
||||
},
|
||||
INTERMEDIATE: {
|
||||
key: 'intermediate',
|
||||
label: 'Intermédiaire',
|
||||
value: 2,
|
||||
skillDistribution: {
|
||||
level0: 4,
|
||||
level1: 2,
|
||||
level2: 1,
|
||||
level3: 0
|
||||
},
|
||||
description: 'Niveau intermédiaire'
|
||||
},
|
||||
REGULAR: {
|
||||
key: 'regular',
|
||||
label: 'Régulier',
|
||||
value: 3,
|
||||
skillDistribution: {
|
||||
level0: 5,
|
||||
level1: 2,
|
||||
level2: 2,
|
||||
level3: 0
|
||||
},
|
||||
description: 'Expérience régulière'
|
||||
},
|
||||
VETERAN: {
|
||||
key: 'veteran',
|
||||
label: 'Vétéran',
|
||||
value: 4,
|
||||
skillDistribution: {
|
||||
level0: 5,
|
||||
level1: 2,
|
||||
level2: 3,
|
||||
level3: 0
|
||||
},
|
||||
description: 'Vétéran expérimenté'
|
||||
},
|
||||
ELITE: {
|
||||
key: 'elite',
|
||||
label: 'Élite',
|
||||
value: 5,
|
||||
skillDistribution: {
|
||||
level0: 6,
|
||||
level1: 3,
|
||||
level2: 2,
|
||||
level3: 1
|
||||
},
|
||||
description: 'Élite, très expérimenté'
|
||||
}
|
||||
};
|
||||
|
||||
export const EXPERIENCE_LEVEL_LIST = [
|
||||
EXPERIENCE_LEVEL.RECRUIT,
|
||||
EXPERIENCE_LEVEL.ROOKIE,
|
||||
EXPERIENCE_LEVEL.INTERMEDIATE,
|
||||
EXPERIENCE_LEVEL.REGULAR,
|
||||
EXPERIENCE_LEVEL.VETERAN,
|
||||
EXPERIENCE_LEVEL.ELITE
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Rôles (Crew roles in a starship)
|
||||
// ============================================================================
|
||||
|
||||
// Caractéristiques par rôle - définit quelles caractéristiques sont prioritaires
|
||||
// pour chaque rôle (High, Medium, Low)
|
||||
export const CHARACTERISTIC_PRIORITIES = {
|
||||
pilot: {
|
||||
high: ['DEX', 'INT'],
|
||||
medium: ['EDU', 'STR'],
|
||||
low: ['END', 'SOC']
|
||||
},
|
||||
navigator: {
|
||||
high: ['INT', 'EDU'],
|
||||
medium: ['DEX', 'SOC'],
|
||||
low: ['STR', 'END']
|
||||
},
|
||||
engineer: {
|
||||
high: ['INT', 'EDU'],
|
||||
medium: ['DEX', 'END'],
|
||||
low: ['STR', 'SOC']
|
||||
},
|
||||
steward: {
|
||||
high: ['INT', 'SOC'],
|
||||
medium: ['DEX', 'EDU'],
|
||||
low: ['STR', 'END']
|
||||
},
|
||||
medic: {
|
||||
high: ['INT', 'EDU'],
|
||||
medium: ['DEX', 'SOC'],
|
||||
low: ['STR', 'END']
|
||||
},
|
||||
marine: {
|
||||
high: ['STR', 'END'],
|
||||
medium: ['DEX', 'INT'],
|
||||
low: ['EDU', 'SOC']
|
||||
},
|
||||
gunner: {
|
||||
high: ['DEX', 'INT'],
|
||||
medium: ['END', 'EDU'],
|
||||
low: ['STR', 'SOC']
|
||||
},
|
||||
scout: {
|
||||
high: ['DEX', 'INT'],
|
||||
medium: ['END', 'EDU'],
|
||||
low: ['STR', 'SOC']
|
||||
},
|
||||
technician: {
|
||||
high: ['INT', 'EDU'],
|
||||
medium: ['DEX', 'END'],
|
||||
low: ['STR', 'SOC']
|
||||
},
|
||||
leader: {
|
||||
high: ['INT', 'SOC'],
|
||||
medium: ['EDU', 'END'],
|
||||
low: ['DEX', 'STR']
|
||||
},
|
||||
diplomat: {
|
||||
high: ['INT', 'SOC'],
|
||||
medium: ['EDU', 'DEX'],
|
||||
low: ['STR', 'END']
|
||||
},
|
||||
entertainer: {
|
||||
high: ['DEX', 'SOC'],
|
||||
medium: ['INT', 'EDU'],
|
||||
low: ['STR', 'END']
|
||||
},
|
||||
trader: {
|
||||
high: ['INT', 'SOC'],
|
||||
medium: ['EDU', 'DEX'],
|
||||
low: ['STR', 'END']
|
||||
},
|
||||
thug: {
|
||||
high: ['STR', 'END'],
|
||||
medium: ['DEX', 'INT'],
|
||||
low: ['EDU', 'SOC']
|
||||
}
|
||||
};
|
||||
|
||||
// Compétences pertinentes pour chaque rôle
|
||||
export const ROLE_SKILLS = {
|
||||
pilot: [
|
||||
'Pilot-Spacecraft',
|
||||
'Astrogation',
|
||||
'Electronics-Sensors',
|
||||
'Gunner',
|
||||
'Mechanic',
|
||||
'Pilot-Small Craft',
|
||||
'Leadership',
|
||||
'Vacc Suit',
|
||||
'Communications',
|
||||
'Drive-Grav',
|
||||
'Survival',
|
||||
'Recon',
|
||||
'Flyer'
|
||||
],
|
||||
navigator: [
|
||||
'Astrogation',
|
||||
'Electronics-Sensors',
|
||||
'Pilot-Spacecraft',
|
||||
'Computers',
|
||||
'Survival',
|
||||
'Navigation',
|
||||
'Mechanic',
|
||||
'Leadership',
|
||||
'Tactics',
|
||||
'Engineer',
|
||||
'Vacc Suit',
|
||||
'Recon'
|
||||
],
|
||||
engineer: [
|
||||
'Engineer-MDrive',
|
||||
'Mechanic',
|
||||
'Engineer-Power',
|
||||
'Computers',
|
||||
'Engineer-JDrive',
|
||||
'Engineer-Life Support',
|
||||
'Electronics-Sensors',
|
||||
'Survival',
|
||||
'Pilot-Small Craft',
|
||||
'Leadership',
|
||||
'Vacc Suit',
|
||||
'Recon',
|
||||
'Drive'
|
||||
],
|
||||
steward: [
|
||||
'Steward',
|
||||
'Carouse',
|
||||
'Persuade',
|
||||
'Broker',
|
||||
'Admin',
|
||||
'Computers',
|
||||
'Language',
|
||||
'Advocate',
|
||||
'Leadership',
|
||||
'Medic',
|
||||
'Streetwise',
|
||||
'Diplomat'
|
||||
],
|
||||
medic: [
|
||||
'Medic',
|
||||
'Science-Biology',
|
||||
'Science-Chemistry',
|
||||
'Deception',
|
||||
'Investigate',
|
||||
'Diplomat',
|
||||
'Computers',
|
||||
'Persuade',
|
||||
'Admin',
|
||||
'Broker',
|
||||
'Electronics-Sensors',
|
||||
'Drive',
|
||||
'Leadership'
|
||||
],
|
||||
marine: [
|
||||
'Gun Combat',
|
||||
'Survival',
|
||||
'Athletics-Strength',
|
||||
'Melee-Unarmed',
|
||||
'Heavy Weapons',
|
||||
'Tactics',
|
||||
'Recon',
|
||||
'Electronics-Sensors',
|
||||
'Leadership',
|
||||
'Medic',
|
||||
'Drive-Grav',
|
||||
'Communications',
|
||||
'Stealth'
|
||||
],
|
||||
gunner: [
|
||||
'Gunner-Turrets',
|
||||
'Electronics-Sensors',
|
||||
'Gunner-Screens',
|
||||
'Tactics',
|
||||
'Gun Combat',
|
||||
'Leadership',
|
||||
'Mechanic',
|
||||
'Heavy Weapons',
|
||||
'Explosives',
|
||||
'Computers',
|
||||
'Pilot-Small Craft',
|
||||
'Athletics-Dexterity',
|
||||
'Melee-Blade'
|
||||
],
|
||||
scout: [
|
||||
'Survival',
|
||||
'Recon',
|
||||
'Pilot-Small Craft',
|
||||
'Astrogation',
|
||||
'Electronics-Sensors',
|
||||
'Stealth',
|
||||
'Gunner',
|
||||
'Medic',
|
||||
'Tactics',
|
||||
'Gun Combat',
|
||||
'Navigation',
|
||||
'Leadership'
|
||||
],
|
||||
technician: [
|
||||
'Mechanic',
|
||||
'Computers',
|
||||
'Electronics-Sensors',
|
||||
'Engineer-Power',
|
||||
'Engineer-MDrive',
|
||||
'Drive',
|
||||
'Pilot',
|
||||
'Vacc Suit',
|
||||
'Recon',
|
||||
'Athletics-Dexterity',
|
||||
'Survival',
|
||||
'Explosives'
|
||||
],
|
||||
leader: [
|
||||
'Leadership',
|
||||
'Tactics',
|
||||
'Admin',
|
||||
'Diplomat',
|
||||
'Persuade',
|
||||
'Advocate',
|
||||
'Electronics-Sensors',
|
||||
'Computers',
|
||||
'Deception',
|
||||
'Pilot-Spacecraft',
|
||||
'Engineer',
|
||||
'Recon',
|
||||
'Medic'
|
||||
],
|
||||
diplomat: [
|
||||
'Diplomat',
|
||||
'Persuade',
|
||||
'Advocate',
|
||||
'Admin',
|
||||
'Carouse',
|
||||
'Steward',
|
||||
'Streetwise',
|
||||
'Language',
|
||||
'Broker',
|
||||
'Leadership',
|
||||
'Communications',
|
||||
'Tactics'
|
||||
],
|
||||
entertainer: [
|
||||
'Carouse',
|
||||
'Streetwise',
|
||||
'Art-Instrument',
|
||||
'Persuade',
|
||||
'Stealth',
|
||||
'Deception',
|
||||
'Diplomat',
|
||||
'Art-Acting',
|
||||
'Computers',
|
||||
'Electronics-Sensors',
|
||||
'Leadership',
|
||||
'Broker',
|
||||
'Melee-Blade',
|
||||
'Admin'
|
||||
],
|
||||
trader: [
|
||||
'Broker',
|
||||
'Persuade',
|
||||
'Admin',
|
||||
'Advocate',
|
||||
'Computers',
|
||||
'Streetwise',
|
||||
'Gun Combat',
|
||||
'Diplomat',
|
||||
'Deception',
|
||||
'Carouse',
|
||||
'Communications',
|
||||
'Mechanic',
|
||||
'Electronics-Sensors',
|
||||
'Leadership'
|
||||
],
|
||||
thug: [
|
||||
'Melee-Unarmed',
|
||||
'Gun Combat',
|
||||
'Melee-Blade',
|
||||
'Athletics-Strength',
|
||||
'Stealth',
|
||||
'Streetwise',
|
||||
'Carouse',
|
||||
'Tactics',
|
||||
'Stealth',
|
||||
'Survival',
|
||||
'Persuade',
|
||||
'Explosives',
|
||||
'Computers'
|
||||
]
|
||||
};
|
||||
|
||||
// Liste des rôles avec libellés en français
|
||||
export const ROLE = {
|
||||
PILOT: { key: 'pilot', label: 'Pilote', description: 'Pilote de vaisseau spatial' },
|
||||
NAVIGATOR: { key: 'navigator', label: 'Navigateur', description: 'Navigateur spatial' },
|
||||
ENGINEER: { key: 'engineer', label: 'Ingénieur', description: 'Ingénieur de bord' },
|
||||
STEWARD: { key: 'steward', label: 'Intendant', description: 'Intendant / steward' },
|
||||
MEDIC: { key: 'medic', label: 'Médecin', description: 'Médecin de bord' },
|
||||
MARINE: { key: 'marine', label: 'Marine', description: 'Marine / soldat' },
|
||||
GUNNER: { key: 'gunner', label: 'Artilleur', description: 'Artilleur / canonnier' },
|
||||
SCOUT: { key: 'scout', label: 'Éclaireur', description: 'Éclaireur' },
|
||||
TECHNICIAN: { key: 'technician', label: 'Technicien', description: 'Technicien' },
|
||||
LEADER: { key: 'leader', label: 'Chef', description: 'Chef / leader' },
|
||||
DIPLOMAT: { key: 'diplomat', label: 'Diplomate', description: 'Diplomate' },
|
||||
ENTERTAINER: { key: 'entertainer', label: 'Artiste', description: 'Artiste / divertisseur' },
|
||||
TRADER: { key: 'trader', label: 'Marchand', description: 'Marchand / commerçant' },
|
||||
THUG: { key: 'thug', label: 'Brute', description: 'Brute / voyou' }
|
||||
};
|
||||
|
||||
export const ROLE_LIST = [
|
||||
ROLE.PILOT,
|
||||
ROLE.NAVIGATOR,
|
||||
ROLE.ENGINEER,
|
||||
ROLE.STEWARD,
|
||||
ROLE.MEDIC,
|
||||
ROLE.MARINE,
|
||||
ROLE.GUNNER,
|
||||
ROLE.SCOUT,
|
||||
ROLE.TECHNICIAN,
|
||||
ROLE.LEADER,
|
||||
ROLE.DIPLOMAT,
|
||||
ROLE.ENTERTAINER,
|
||||
ROLE.TRADER,
|
||||
ROLE.THUG
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Genre
|
||||
// ============================================================================
|
||||
|
||||
export const GENDER = {
|
||||
UNSPECIFIED: { key: 'unspecified', label: 'Non spécifié', value: 0 },
|
||||
FEMALE: { key: 'female', label: 'Féminin', value: 1 },
|
||||
MALE: { key: 'male', label: 'Masculin', value: 2 }
|
||||
};
|
||||
|
||||
export const GENDER_LIST = [
|
||||
GENDER.UNSPECIFIED,
|
||||
GENDER.FEMALE,
|
||||
GENDER.MALE
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Catalogues de noms
|
||||
// ============================================================================
|
||||
|
||||
export const NAME_CATALOGS = {
|
||||
surnames: [
|
||||
'Anderson', 'Berezovsky', 'Brown', 'Chen', 'Clark', 'Davis', 'Fujita', 'Garcia',
|
||||
'Gupta', 'Harris', 'Hicks', 'Ito', 'Ivanov', 'Jackson', 'Johnson', 'Jones',
|
||||
'Kim', 'Kobayashi', 'Kowalski', 'Kumar', 'Kuznetsoff', 'Kuznetsov', 'Kuznetsova',
|
||||
'Lee', 'Martin', 'Martinez', 'Miller', 'Moore', 'Nakamura', 'Nguyen', 'Nowak',
|
||||
"O'Brien", "O'Callaghan", "O'Connell", "O'Connor", "O'Keefe", "O'Leary",
|
||||
"O'Malley", "O'Neil", "O'Reilly", "O'Sullivan", 'Park', 'Patel', 'Pierzynski',
|
||||
'Pietrzykowski', 'Pisarski', 'Reshevsky', 'Robinson', 'Rumkowska', 'Saito',
|
||||
'Singh', 'Smith', 'Tanaka', 'Taylor', 'Thomas', 'Thompson', 'Vasquez',
|
||||
'Watanabe', 'White', 'Williams', 'Wilson', 'Wong', 'Yamamoto', 'Yang'
|
||||
],
|
||||
nonGenderedNames: [
|
||||
'Arrow', 'Artemis', 'Ash', 'Aster', 'Avery', 'Basil', 'Ever', 'Fig', 'Finch',
|
||||
'Indigo', 'Jett', 'Juniper', 'Kavi', 'Kaviya', 'Kaviyan', 'Kaviyanan', 'Kay',
|
||||
'Lark', 'Noah', 'Ocean', 'Phoenix', 'Riley', 'River', 'Rory', 'Rowan', 'Sage',
|
||||
'Sawyer', 'Shiloh', 'Sparrow', 'Sutton', 'Tavi', 'Uli', 'Veer', 'Vesper',
|
||||
'Winter', 'Wren', 'Zen', 'Zenith', 'Zephyr', 'Zephyrus'
|
||||
],
|
||||
femaleNames: [
|
||||
'Aarohi', 'Aarushi', 'Abigail', 'Amelia', 'Ananya', 'Anika', 'Anjali',
|
||||
'Anushka', 'Aria', 'Ava', 'Chloe', 'Devi', 'Elina', 'Elizabeth', 'Emily',
|
||||
'Emma', 'Esha', 'Evelyn', 'Grace', 'Harper', 'Isabella', 'Ishani', 'Ishika',
|
||||
'Ishita', 'Layla', 'Lily', 'Madison', 'Margarita', 'Maria', 'Mia', 'Nisha',
|
||||
'Nora', 'Nosheen', 'Olivia', 'Penelope', 'Priya', 'Qadira', 'Riley', 'Scarlett',
|
||||
'Sofia', 'Sophia', 'Tala', 'Ulka', 'Uthra', 'Uthraa', 'Victoria', 'Yasmin',
|
||||
'Zara', 'Zoey', 'Zoya', 'Zulaikha'
|
||||
],
|
||||
maleNames: [
|
||||
'Ahmed', 'Aiden', 'Alexander', 'Benjamin', 'Callum', 'Carter', 'Daniel',
|
||||
'David', 'Dylan', 'Elias', 'Elijah', 'Ethan', 'Ewan', 'Ezra', 'Gavin',
|
||||
'Henry', 'Hudson', 'Jackson', 'Jacob', 'Jaden', 'James', 'Jaxon', 'Jayden',
|
||||
'Jordan', 'Joseph', 'Julian', 'Kian', 'Kianan', 'Kianu', 'Lachlan', 'Leo',
|
||||
'Levi', 'Liam', 'Lincoln', 'Logan', 'Lucas', 'Mason', 'Mateo', 'Matthew',
|
||||
'Michael', 'Mohammed', 'Noah', 'Nolan', 'Oliver', 'Pol', 'Samuel',
|
||||
'Santiago', 'Sebastian', 'Theodore', 'Ulric', 'Wallid', 'William', 'Wyatt'
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Caractéristiques
|
||||
// ============================================================================
|
||||
|
||||
export const CHARACTERISTIC = {
|
||||
STR: { key: 'STR', label: 'Force', mgt2eKey: 'STR' },
|
||||
DEX: { key: 'DEX', label: 'Dextérité', mgt2eKey: 'DEX' },
|
||||
END: { key: 'END', label: 'Endurance', mgt2eKey: 'END' },
|
||||
INT: { key: 'INT', label: 'Intellect', mgt2eKey: 'INT' },
|
||||
EDU: { key: 'EDU', label: 'Éducation', mgt2eKey: 'EDU' },
|
||||
SOC: { key: 'SOC', label: 'Statut Social', mgt2eKey: 'SOC' }
|
||||
};
|
||||
|
||||
export const CHARACTERISTIC_LIST = [
|
||||
CHARACTERISTIC.STR,
|
||||
CHARACTERISTIC.DEX,
|
||||
CHARACTERISTIC.END,
|
||||
CHARACTERISTIC.INT,
|
||||
CHARACTERISTIC.EDU,
|
||||
CHARACTERISTIC.SOC
|
||||
];
|
||||
|
||||
// Ordre des caractéristiques pour l'UPP
|
||||
export const UPP_ORDER = ['STR', 'DEX', 'END', 'INT', 'EDU', 'SOC'];
|
||||
|
||||
// ============================================================================
|
||||
// Fonctions utilitaires
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convertit une valeur de caractéristique en code hexadécimal pour UPP
|
||||
* @param {number} value - Valeur de la caractéristique (0-15)
|
||||
* @returns {string} - Code hexadécimal
|
||||
*/
|
||||
export function toHex(value) {
|
||||
return Math.max(0, Math.min(15, Math.floor(value))).toString(16).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le DM (Damage Modifier) à partir d'une valeur de caractéristique
|
||||
* @param {number} value - Valeur de la caractéristique
|
||||
* @returns {number} - Modificateur de dégâts
|
||||
*/
|
||||
export function calculateDm(value) {
|
||||
return Math.floor((value - 6) / 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un nom aléatoire à partir des catalogues
|
||||
* @param {string} genderKey - Clé du genre ('unspecified', 'female', 'male')
|
||||
* @returns {{firstName: string, surname: string, fullName: string}}
|
||||
*/
|
||||
export function generateRandomName(genderKey = 'unspecified') {
|
||||
const genderMap = {
|
||||
unspecified: NAME_CATALOGS.nonGenderedNames,
|
||||
female: NAME_CATALOGS.femaleNames,
|
||||
male: NAME_CATALOGS.maleNames
|
||||
};
|
||||
|
||||
const firstNames = genderMap[genderKey] || NAME_CATALOGS.nonGenderedNames;
|
||||
const surnames = NAME_CATALOGS.surnames;
|
||||
|
||||
const firstName = pickRandomItem(firstNames);
|
||||
const surname = pickRandomItem(surnames);
|
||||
|
||||
return {
|
||||
firstName,
|
||||
surname,
|
||||
fullName: `${firstName} ${surname}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélectionne un élément aléatoire dans un tableau
|
||||
* @template T
|
||||
* @param {T[]} items - Tableau d'éléments
|
||||
* @returns {T} - Élément sélectionné
|
||||
*/
|
||||
export function pickRandomItem(items) {
|
||||
if (!items || items.length === 0) {
|
||||
throw new Error('Cannot pick from empty array');
|
||||
}
|
||||
const index = Math.floor(Math.random() * items.length);
|
||||
return items[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mélange un tableau (algorithme de Fisher-Yates)
|
||||
* @template T
|
||||
* @param {T[]} array - Tableau à mélanger
|
||||
* @returns {T[]} - Nouveau tableau mélangé
|
||||
*/
|
||||
export function shuffleArray(array) {
|
||||
const newArray = [...array];
|
||||
for (let i = newArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extraire les N premiers éléments d'un tableau et retourner les deux parties
|
||||
* @template T
|
||||
* @param {T[]} array - Tableau source
|
||||
* @param {number} count - Nombre d'éléments à extraire
|
||||
* @returns {[T[], T[]]} - [éléments extraits, éléments restants]
|
||||
*/
|
||||
export function popRandomItems(array, count) {
|
||||
if (!array || array.length === 0 || count <= 0) {
|
||||
return [[], [...array]];
|
||||
}
|
||||
|
||||
const shuffled = shuffleArray(array);
|
||||
const taken = shuffled.slice(0, Math.min(count, shuffled.length));
|
||||
const remaining = shuffled.slice(Math.min(count, shuffled.length));
|
||||
|
||||
return [taken, remaining];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un rôle par sa clé
|
||||
* @param {string} key - Clé du rôle
|
||||
* @returns {Object|null} - Objet rôle ou null
|
||||
*/
|
||||
export function getRoleByKey(key) {
|
||||
const found = ROLE_LIST.find(r => r.key === key);
|
||||
return found || ROLE.PILOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une catégorie de citoyen par sa clé
|
||||
* @param {string} key - Clé de la catégorie
|
||||
* @returns {Object} - Objet catégorie
|
||||
*/
|
||||
export function getCitizenCategoryByKey(key) {
|
||||
const found = CITIZEN_CATEGORY_LIST.find(c => c.key === key);
|
||||
return found || CITIZEN_CATEGORY.AVERAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un niveau d'expérience par sa clé
|
||||
* @param {string} key - Clé du niveau
|
||||
* @returns {Object} - Objet niveau d'expérience
|
||||
*/
|
||||
export function getExperienceLevelByKey(key) {
|
||||
const found = EXPERIENCE_LEVEL_LIST.find(e => e.key === key);
|
||||
return found || EXPERIENCE_LEVEL.REGULAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un genre par sa clé
|
||||
* @param {string} key - Clé du genre
|
||||
* @returns {Object} - Objet genre
|
||||
*/
|
||||
export function getGenderByKey(key) {
|
||||
const found = GENDER_LIST.find(g => g.key === key);
|
||||
return found || GENDER.UNSPECIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les compétences pour un rôle
|
||||
* @param {string} roleKey - Clé du rôle
|
||||
* @returns {string[]} - Tableau de compétences
|
||||
*/
|
||||
export function getSkillsForRole(roleKey) {
|
||||
return ROLE_SKILLS[roleKey] || ROLE_SKILLS.pilot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les priorités de caractéristiques pour un rôle
|
||||
* @param {string} roleKey - Clé du rôle
|
||||
* @returns {Object} - Priorités de caractéristiques
|
||||
*/
|
||||
export function getCharacteristicPrioritiesForRole(roleKey) {
|
||||
return CHARACTERISTIC_PRIORITIES[roleKey] || CHARACTERISTIC_PRIORITIES.pilot;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
};
|
||||
+41
-2
@@ -1,4 +1,5 @@
|
||||
import { NpcDialog } from './NpcDialog.js';
|
||||
import { TravellerNpcDialog, openTravellerNpcDialog } from './TravellerNpcDialog.js';
|
||||
import { syncNpcRollTables } from './npcRollTableSync.js';
|
||||
import './mgt2eMigration.js';
|
||||
|
||||
@@ -9,6 +10,10 @@ function openNpcDialog(initialTab, options = {}) {
|
||||
new NpcDialog({ initialTab, ...options }).render({ force: true });
|
||||
}
|
||||
|
||||
function openTravellerNpcGenerator() {
|
||||
openTravellerNpcDialog();
|
||||
}
|
||||
|
||||
function registerNpcCommand(commandName, initialTab) {
|
||||
if (!ChatLogV2?.CHAT_COMMANDS) {
|
||||
console.warn(`${MODULE_ID} | ChatLog.CHAT_COMMANDS indisponible, commande /${commandName} non enregistrée`);
|
||||
@@ -25,6 +30,22 @@ function registerNpcCommand(commandName, initialTab) {
|
||||
console.log(`${MODULE_ID} | Commande /${commandName} enregistrée via ChatLog.CHAT_COMMANDS`);
|
||||
}
|
||||
|
||||
function registerTravellerNpcCommand() {
|
||||
if (!ChatLogV2?.CHAT_COMMANDS) {
|
||||
console.warn(`${MODULE_ID} | ChatLog.CHAT_COMMANDS indisponible, commande /gennpc non enregistrée`);
|
||||
return;
|
||||
}
|
||||
|
||||
ChatLogV2.CHAT_COMMANDS.gennpc = {
|
||||
rgx: new RegExp(`^\\/gennpc(?:\\s+(.*))?$`, 'i'),
|
||||
fn: () => {
|
||||
openTravellerNpcGenerator();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
console.log(`${MODULE_ID} | Commande /gennpc enregistrée via ChatLog.CHAT_COMMANDS`);
|
||||
}
|
||||
|
||||
Hooks.once('init', () => {
|
||||
console.log(`${MODULE_ID} | Outils PNJ initialisés`);
|
||||
|
||||
@@ -35,17 +56,20 @@ Hooks.once('init', () => {
|
||||
loadTemplatesFn([
|
||||
`modules/${MODULE_ID}/templates/npc-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/npc-result.hbs`,
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
||||
]);
|
||||
}
|
||||
|
||||
registerNpcCommand('pnj', 'npc');
|
||||
registerNpcCommand('rencontre', 'encounter');
|
||||
registerNpcCommand('mission', 'mission');
|
||||
registerTravellerNpcCommand();
|
||||
});
|
||||
|
||||
Hooks.once('ready', async () => {
|
||||
await syncNpcRollTables();
|
||||
console.log(`${MODULE_ID} | Outils PNJ prêts – tapez /pnj, /rencontre ou /mission dans le chat`);
|
||||
console.log(`${MODULE_ID} | Outils PNJ prêts – tapez /pnj, /rencontre, /mission ou /gennpc dans le chat`);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -91,13 +115,18 @@ Hooks.on('renderChatInput', (app, html, data) => {
|
||||
event.stopImmediatePropagation();
|
||||
openNpcDialog('mission');
|
||||
input.val('');
|
||||
} else if (content?.startsWith('/gennpc')) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
openTravellerNpcGenerator();
|
||||
input.val('');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Intercepte les messages de chat pour /pnj, /rencontre, /mission
|
||||
* Intercepte les messages de chat pour /pnj, /rencontre, /mission, /gennpc
|
||||
* Utilise preCreateChatMessage pour Foundry v14+ (avant que le message ne soit validé)
|
||||
* Compatible avec Foundry v13 et v14
|
||||
*/
|
||||
@@ -118,6 +147,11 @@ Hooks.on('preCreateChatMessage', (message, data, options) => {
|
||||
openNpcDialog('mission');
|
||||
return false; // Empêche la création du message
|
||||
}
|
||||
|
||||
if (content === '/gennpc' || content?.startsWith('/gennpc ')) {
|
||||
openTravellerNpcGenerator();
|
||||
return false; // Empêche la création du message
|
||||
}
|
||||
});
|
||||
|
||||
// Gardé pour compatibilité v13
|
||||
@@ -152,4 +186,9 @@ Hooks.on('chatMessage', (...args) => {
|
||||
openNpcDialog('mission');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trimmed === '/gennpc' || trimmed?.startsWith('/gennpc ')) {
|
||||
openTravellerNpcGenerator();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
/**
|
||||
* 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,
|
||||
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,
|
||||
getExperienceLevelByKey,
|
||||
getGenderByKey,
|
||||
getSkillsForRole,
|
||||
getCharacteristicPrioritiesForRole,
|
||||
DEFAULT_OPTIONS
|
||||
} from './data/travellerNpcGenerator.js';
|
||||
|
||||
import { setSkillLevel, localizeSkill } from './mgt2eSkills.js';
|
||||
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
// Cache pour le système de base des acteurs mgt2e
|
||||
let mgt2eBaseActorSystemPromise = null;
|
||||
|
||||
// ============================================================================
|
||||
// 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);
|
||||
|
||||
// On commence avec l'array de base de la catégorie
|
||||
let characteristicArray = [...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;
|
||||
});
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
// 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 => {
|
||||
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 (level0, level1, level2, level3)
|
||||
* @returns {Map<string, number>} - Map des compétences avec leurs 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)
|
||||
const uniqueSkills = [];
|
||||
const seenSkills = new Set();
|
||||
for (const skill of remainingSkills) {
|
||||
const baseSkill = skill.split('-')[0];
|
||||
if (!seenSkills.has(baseSkill)) {
|
||||
seenSkills.add(baseSkill);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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é
|
||||
*/
|
||||
export function generateTravellerNpc(options = {}) {
|
||||
// Fusionner avec les options par défaut
|
||||
const opts = {
|
||||
...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 {
|
||||
name = {
|
||||
firstName: pickRandomItem(
|
||||
opts.gender === 'female' ? NAME_CATALOGS.femaleNames :
|
||||
opts.gender === 'male' ? NAME_CATALOGS.maleNames :
|
||||
NAME_CATALOGS.nonGenderedNames
|
||||
),
|
||||
surname: pickRandomItem(NAME_CATALOGS.surnames),
|
||||
fullName: ''
|
||||
};
|
||||
name.fullName = `${name.firstName} ${name.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
|
||||
const characteristicLabels = {};
|
||||
CHARACTERISTIC_LIST.forEach(char => {
|
||||
characteristicLabels[char.key] = char.label;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'traveller-npc',
|
||||
name,
|
||||
role,
|
||||
citizenCategory,
|
||||
experience,
|
||||
gender,
|
||||
characteristics,
|
||||
upp,
|
||||
skills,
|
||||
skillsForActor, // Compétences au format mgt2e
|
||||
MODULE_ID,
|
||||
UPP_ORDER,
|
||||
// Métadonnées pour l'affichage
|
||||
display: {
|
||||
roleLabel: role.label,
|
||||
categoryLabel: citizenCategory.label,
|
||||
experienceLabel: experience.label,
|
||||
genderLabel: gender.label,
|
||||
characteristicLabels
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Création de la fiche d'acteur
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
const system = await mgt2eBaseActorSystemPromise;
|
||||
return system ? foundry.utils.deepClone(system) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les caractéristiques au format mgt2e
|
||||
*
|
||||
* @param {Object} existingCharacteristics - Caractéristiques existantes (optionnel)
|
||||
* @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)) {
|
||||
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 (optionnel)
|
||||
* @param {Array<{name: string, level: number}>} skills - Compétences générées
|
||||
* @returns {Object} - Compétences au format mgt2e
|
||||
*/
|
||||
export function buildMgt2eSkills(existingSkills = {}, skills, useMgt2eFormat = 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);
|
||||
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 => {
|
||||
// Essayer de localiser la compétence
|
||||
try {
|
||||
return localizeSkill(s.name);
|
||||
} catch (e) {
|
||||
return s.name;
|
||||
}
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
return [
|
||||
`${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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 baseActorSystem = game.system?.id === 'mgt2e' ? await getMgt2eBaseActorSystem() : null;
|
||||
|
||||
const actorName = requestedName || npcData.name.fullName || `PNJ — ${npcData.role.label}`;
|
||||
|
||||
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), // Âge entre 18 et 58 ans
|
||||
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 // Utiliser le format mgt2e directement
|
||||
),
|
||||
description: buildActorDescription(npcData, actorName),
|
||||
},
|
||||
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()
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 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) {
|
||||
actor.sheet?.render(true);
|
||||
}
|
||||
|
||||
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}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fonction principale exportée
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export async function generateAndCreateTravellerNpc(options = {}) {
|
||||
const npcData = generateTravellerNpc(options);
|
||||
|
||||
let actor = null;
|
||||
if (options.createActor) {
|
||||
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 fonctions utilitaires pour les tests
|
||||
// ============================================================================
|
||||
|
||||
export {
|
||||
generateCharacteristics,
|
||||
generateSkills,
|
||||
generateName,
|
||||
buildMgt2eCharacteristics,
|
||||
buildMgt2eSkills,
|
||||
// Ré-exporter les données
|
||||
CITIZEN_CATEGORY,
|
||||
CITIZEN_CATEGORY_LIST,
|
||||
EXPERIENCE_LEVEL,
|
||||
EXPERIENCE_LEVEL_LIST,
|
||||
ROLE,
|
||||
ROLE_LIST,
|
||||
GENDER,
|
||||
GENDER_LIST,
|
||||
CHARACTERISTIC,
|
||||
CHARACTERISTIC_LIST,
|
||||
UPP_ORDER,
|
||||
NAME_CATALOGS
|
||||
};
|
||||
Reference in New Issue
Block a user