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:
+7
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "mgt2-compendium-amiral-denisov",
|
||||
"title": "MgT2e - Compendium Amiral Denisov",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "13",
|
||||
@@ -11,8 +11,13 @@
|
||||
"esmodules": [
|
||||
"scripts/commerce.js",
|
||||
"scripts/npc.js",
|
||||
"scripts/utils/travellerNpcUtils.js",
|
||||
"scripts/data/travellerNpcGenerator.js",
|
||||
"scripts/travellerNpcGenerator.js",
|
||||
"scripts/TravellerNpcDialog.js",
|
||||
"scripts/travellerNpcGenerator.js"
|
||||
"scripts/mgt2eMigration.js",
|
||||
"scripts/npcRollTableSync.js",
|
||||
"scripts/mgt2eSkills.js"
|
||||
],
|
||||
"styles": [
|
||||
"styles/commerce.css",
|
||||
|
||||
@@ -11,8 +11,11 @@ import {
|
||||
ROLE_LIST,
|
||||
GENDER_LIST,
|
||||
DEFAULT_OPTIONS,
|
||||
CHARACTERISTIC_LIST,
|
||||
UPP_ORDER
|
||||
generateRandomName,
|
||||
CITIZEN_CATEGORY_LABELS_FR,
|
||||
EXPERIENCE_LEVEL_LABELS_FR,
|
||||
ROLE_LABELS_FR,
|
||||
GENDER_LABELS_FR
|
||||
} from './data/travellerNpcGenerator.js';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
@@ -50,11 +53,18 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
|
||||
gender: options.gender || DEFAULT_OPTIONS.gender,
|
||||
firstName: options.firstName || '',
|
||||
surname: options.surname || '',
|
||||
useRandomName: options.useRandomName !== false, // Par défaut, on utilise des noms aléatoires
|
||||
useRandomName: options.useRandomName !== false,
|
||||
createActor: options.createActor !== undefined ? options.createActor : DEFAULT_OPTIONS.createActor,
|
||||
actorName: options.actorName || '',
|
||||
openCreatedActor: options.openCreatedActor !== undefined ? options.openCreatedActor : DEFAULT_OPTIONS.openCreatedActor,
|
||||
};
|
||||
|
||||
// Bind les méthodes pour éviter les problèmes de contexte
|
||||
this._readForm = this._readForm.bind(this);
|
||||
this._handleGenerate = this._handleGenerate.bind(this);
|
||||
this._randomizeName = this._randomizeName.bind(this);
|
||||
this._applyThemeStyles = this._applyThemeStyles.bind(this);
|
||||
this._getForm = this._getForm.bind(this);
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
@@ -63,22 +73,22 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
|
||||
...this._formData,
|
||||
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
|
||||
key: c.key,
|
||||
label: c.label,
|
||||
label: CITIZEN_CATEGORY_LABELS_FR[c.key] || c.label,
|
||||
description: c.description
|
||||
})),
|
||||
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
|
||||
key: e.key,
|
||||
label: e.label,
|
||||
label: EXPERIENCE_LEVEL_LABELS_FR[e.key] || e.label,
|
||||
description: e.description
|
||||
})),
|
||||
roles: ROLE_LIST.map(r => ({
|
||||
key: r.key,
|
||||
label: r.label,
|
||||
label: ROLE_LABELS_FR[r.key] || r.label,
|
||||
description: r.description
|
||||
})),
|
||||
genders: GENDER_LIST.map(g => ({
|
||||
key: g.key,
|
||||
label: g.label
|
||||
label: GENDER_LABELS_FR[g.key] || g.label
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -106,6 +116,7 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
|
||||
// Gestion du basculement entre nom aléatoire et nom personnalisé
|
||||
html.find('[name="useRandomName"]').on('change', (event) => {
|
||||
const useRandom = event.target.checked;
|
||||
this._formData.useRandomName = useRandom;
|
||||
html.find('.name-fields').toggleClass('hidden', useRandom);
|
||||
});
|
||||
|
||||
@@ -118,31 +129,9 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
|
||||
}
|
||||
|
||||
_applyThemeStyles(html) {
|
||||
// Appliquer les styles de thème cohérents avec le dialogue existant
|
||||
html.find('.tabs .item').css({
|
||||
color: '#d8c79a',
|
||||
'text-shadow': 'none',
|
||||
'background-color': '',
|
||||
'border-bottom-color': 'transparent'
|
||||
});
|
||||
|
||||
html.find('.tabs .item.active').css({
|
||||
color: '#d9b24c',
|
||||
'text-shadow': 'none',
|
||||
'background-color': 'rgba(201, 162, 39, 0.18)',
|
||||
'border-bottom-color': '#c9a227'
|
||||
});
|
||||
|
||||
html.find('h3').css({
|
||||
color: '#5f4300',
|
||||
'border-bottom-color': '#b78f26',
|
||||
'text-shadow': 'none'
|
||||
});
|
||||
|
||||
html.find('legend').css({
|
||||
color: '#7a5c00',
|
||||
'text-shadow': 'none'
|
||||
});
|
||||
// Les styles sont maintenant gérés par CSS, cette méthode peut être vide
|
||||
// ou utilisée pour des ajustements spécifiques si nécessaire
|
||||
// Les styles de base sont cohérents avec mgt2-npc-dialog et mgt2-commerce-dialog
|
||||
}
|
||||
|
||||
_readForm(html) {
|
||||
@@ -159,17 +148,14 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
|
||||
}
|
||||
|
||||
_randomizeName(html) {
|
||||
// Importer dynamiquement pour éviter les dépendances circulaires
|
||||
import('./data/travellerNpcGenerator.js').then(module => {
|
||||
const name = module.generateRandomName(this._formData.gender);
|
||||
html.find('[name="firstName"]').val(name.firstName);
|
||||
html.find('[name="surname"]').val(name.surname);
|
||||
this._formData.firstName = name.firstName;
|
||||
this._formData.surname = name.surname;
|
||||
this._formData.useRandomName = false;
|
||||
html.find('[name="useRandomName"]').prop('checked', false);
|
||||
html.find('.name-fields').removeClass('hidden');
|
||||
});
|
||||
const name = generateRandomName(this._formData.gender);
|
||||
html.find('[name="firstName"]').val(name.firstName);
|
||||
html.find('[name="surname"]').val(name.surname);
|
||||
this._formData.firstName = name.firstName;
|
||||
this._formData.surname = name.surname;
|
||||
this._formData.useRandomName = false;
|
||||
html.find('[name="useRandomName"]').prop('checked', false);
|
||||
html.find('.name-fields').removeClass('hidden');
|
||||
}
|
||||
|
||||
async _handleGenerate() {
|
||||
@@ -238,6 +224,9 @@ export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
// Import des données pour les helpers
|
||||
import { CHARACTERISTIC_LIST, UPP_ORDER } from './data/travellerNpcGenerator.js';
|
||||
|
||||
let helpersRegistered = false;
|
||||
|
||||
function registerHandlebarsHelpers() {
|
||||
@@ -279,23 +268,6 @@ function registerHandlebarsHelpers() {
|
||||
return 'skill-level-0';
|
||||
});
|
||||
|
||||
// Helper pour formater une compétence avec son niveau
|
||||
Handlebars.registerHelper('formatSkillForDisplay', (name, level) => {
|
||||
if (level === 0) {
|
||||
return name;
|
||||
}
|
||||
return `${name}-${level}`;
|
||||
});
|
||||
|
||||
// Helper pour créer un objet de libellés de caractéristiques
|
||||
Handlebars.registerHelper('createCharacteristicLabels', () => {
|
||||
const labels = {};
|
||||
CHARACTERISTIC_LIST.forEach(char => {
|
||||
labels[char.key] = char.label;
|
||||
});
|
||||
return labels;
|
||||
});
|
||||
|
||||
// Helper pour lookup dans un objet
|
||||
Handlebars.registerHelper('lookup', (obj, key) => {
|
||||
if (!obj || !key) return '';
|
||||
|
||||
@@ -8,7 +8,191 @@
|
||||
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
// Données par défaut
|
||||
// ============================================================================
|
||||
|
||||
export const DEFAULT_OPTIONS = {
|
||||
citizenCategory: CITIZEN_CATEGORY.AVERAGE.key,
|
||||
experience: EXPERIENCE_LEVEL.REGULAR.key,
|
||||
role: ROLE.PILOT.key,
|
||||
gender: GENDER.UNSPECIFIED.key,
|
||||
createActor: false,
|
||||
actorName: '',
|
||||
openCreatedActor: true
|
||||
};
|
||||
=======
|
||||
// ============================================================================
|
||||
// Traductions françaises des compétences Traveller
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Libellés français des compétences Traveller
|
||||
* Basé sur les traductions du système mgt2e et les standards Traveller FR
|
||||
*/
|
||||
export const SKILL_LABELS_FR = {
|
||||
// Pilotage
|
||||
'Pilot-Spacecraft': 'Pilote – Vaisseau spatial',
|
||||
'Pilot-Small Craft': 'Pilote – Aéronef léger',
|
||||
'Pilot': 'Pilote',
|
||||
'Flyer': 'Pilote – Aéronef atmosphérique',
|
||||
|
||||
// Navigation
|
||||
'Astrogation': 'Astrogation',
|
||||
'Navigation': 'Navigation',
|
||||
|
||||
// Électronique
|
||||
'Electronics-Sensors': 'Électronique – Capteurs',
|
||||
'Electronics-Communications': 'Électronique – Communications',
|
||||
'Electronics-Computers': 'Électronique – Informatique',
|
||||
'Electronics': 'Électronique',
|
||||
'Computers': 'Informatique',
|
||||
|
||||
// Armement
|
||||
'Gunner-Turrets': 'Artilleur – Tourelles',
|
||||
'Gunner-Screens': 'Artilleur – Boucliers',
|
||||
'Gunner': 'Artilleur',
|
||||
'Gun Combat': 'Combat aux armes à feu',
|
||||
'Heavy Weapons': 'Armes lourdes',
|
||||
'Explosives': 'Explosifs',
|
||||
|
||||
// Mécanique et Ingénierie
|
||||
'Mechanic': 'Mécanique',
|
||||
'Engineer-MDrive': 'Ingénieur – Propulsion manœuvre',
|
||||
'Engineer-Power': 'Ingénieur – Énergie',
|
||||
'Engineer-JDrive': 'Ingénieur – Propulsion saut',
|
||||
'Engineer-Life Support': 'Ingénieur – Support vie',
|
||||
'Engineer': 'Ingénieur',
|
||||
|
||||
// Social et Administration
|
||||
'Steward': 'Intendant',
|
||||
'Carouse': 'Festoyer',
|
||||
'Persuade': 'Persuasion',
|
||||
'Broker': 'Courtage',
|
||||
'Admin': 'Administration',
|
||||
'Advocate': 'Plaidoyer',
|
||||
'Diplomat': 'Diplomatie',
|
||||
'Streetwise': 'Rues',
|
||||
'Leadership': 'Direction',
|
||||
|
||||
// Sciences
|
||||
'Science-Biology': 'Science – Biologie',
|
||||
'Science-Chemistry': 'Science – Chimie',
|
||||
'Science': 'Science',
|
||||
|
||||
// Santé
|
||||
'Medic': 'Médecine',
|
||||
|
||||
// Investigation
|
||||
'Deception': 'Tromperie',
|
||||
'Investigate': 'Investigation',
|
||||
|
||||
// Combat
|
||||
'Melee-Unarmed': 'Mêlée – Sans arme',
|
||||
'Melee-Blade': 'Mêlée – Arme blanche',
|
||||
'Melee': 'Mêlée',
|
||||
'Athletics-Strength': 'Athlétisme – Force',
|
||||
'Athletics-Dexterity': 'Athlétisme – Dextérité',
|
||||
'Athletics': 'Athlétisme',
|
||||
|
||||
// Tactiques
|
||||
'Tactics': 'Tactiques',
|
||||
|
||||
// Reconnaissance et Survie
|
||||
'Recon': 'Reconnaissance',
|
||||
'Survival': 'Survie',
|
||||
'Stealth': 'Discrétion',
|
||||
|
||||
// Communications
|
||||
'Communications': 'Communications',
|
||||
|
||||
// Conduite
|
||||
'Drive-Grav': 'Conduite – Gravité',
|
||||
'Drive': 'Conduite',
|
||||
|
||||
// Autres
|
||||
'Vacc Suit': 'Combinaison spatiale',
|
||||
'Language': 'Langue',
|
||||
'Art-Acting': 'Art – Jeu d\'acteur',
|
||||
'Art-Instrument': 'Art – Instrument',
|
||||
'Art': 'Art'
|
||||
};
|
||||
|
||||
/**
|
||||
* Libellés français des caractéristiques
|
||||
*/
|
||||
export const CHARACTERISTIC_LABELS_FR = {
|
||||
'STR': 'Force',
|
||||
'DEX': 'Dextérité',
|
||||
'END': 'Endurance',
|
||||
'INT': 'Intellect',
|
||||
'EDU': 'Éducation',
|
||||
'SOC': 'Statut Social'
|
||||
};
|
||||
|
||||
/**
|
||||
* Libellés français des catégories de citoyen
|
||||
*/
|
||||
export const CITIZEN_CATEGORY_LABELS_FR = {
|
||||
'belowAverage': 'En dessous de la moyenne',
|
||||
'average': 'Moyenne',
|
||||
'aboveAverage': 'Au-dessus de la moyenne',
|
||||
'exceptional': 'Exceptionnel'
|
||||
};
|
||||
|
||||
/**
|
||||
* Libellés français des niveaux d'expérience
|
||||
*/
|
||||
export const EXPERIENCE_LEVEL_LABELS_FR = {
|
||||
'recruit': 'Recrue',
|
||||
'rookie': 'Débutant',
|
||||
'intermediate': 'Intermédiaire',
|
||||
'regular': 'Régulier',
|
||||
'veteran': 'Vétéran',
|
||||
'elite': 'Élite'
|
||||
};
|
||||
|
||||
/**
|
||||
* Libellés français des rôles
|
||||
*/
|
||||
export const ROLE_LABELS_FR = {
|
||||
'pilot': 'Pilote',
|
||||
'navigator': 'Navigateur',
|
||||
'engineer': 'Ingénieur',
|
||||
'steward': 'Intendant',
|
||||
'medic': 'Médecin',
|
||||
'marine': 'Marine',
|
||||
'gunner': 'Artilleur',
|
||||
'scout': 'Éclaireur',
|
||||
'technician': 'Technicien',
|
||||
'leader': 'Chef',
|
||||
'diplomat': 'Diplomate',
|
||||
'entertainer': 'Artiste',
|
||||
'trader': 'Marchand',
|
||||
'thug': 'Brute'
|
||||
};
|
||||
|
||||
/**
|
||||
* Libellés français des genres
|
||||
*/
|
||||
export const GENDER_LABELS_FR = {
|
||||
'unspecified': 'Non spécifié',
|
||||
'female': 'Féminin',
|
||||
'male': 'Masculin'
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Données par défaut
|
||||
// ============================================================================
|
||||
|
||||
export const DEFAULT_OPTIONS = {
|
||||
citizenCategory: CITIZEN_CATEGORY.AVERAGE.key,
|
||||
experience: EXPERIENCE_LEVEL.REGULAR.key,
|
||||
role: ROLE.PILOT.key,
|
||||
gender: GENDER.UNSPECIFIED.key,
|
||||
createActor: false,
|
||||
actorName: '',
|
||||
openCreatedActor: true
|
||||
};============================================================================
|
||||
// Catégories de citoyens
|
||||
// ============================================================================
|
||||
|
||||
@@ -420,7 +604,6 @@ export const ROLE_SKILLS = {
|
||||
'Streetwise',
|
||||
'Carouse',
|
||||
'Tactics',
|
||||
'Stealth',
|
||||
'Survival',
|
||||
'Persuade',
|
||||
'Explosives',
|
||||
@@ -547,6 +730,21 @@ export const CHARACTERISTIC_LIST = [
|
||||
// Ordre des caractéristiques pour l'UPP
|
||||
export const UPP_ORDER = ['STR', 'DEX', 'END', 'INT', 'EDU', 'SOC'];
|
||||
|
||||
// ============================================================================
|
||||
// Codes d'erreur
|
||||
// ============================================================================
|
||||
|
||||
export const ERROR_CODES = {
|
||||
INVALID_OPTIONS: 'INVALID_OPTIONS',
|
||||
INVALID_ROLE: 'INVALID_ROLE',
|
||||
INVALID_CATEGORY: 'INVALID_CATEGORY',
|
||||
INVALID_EXPERIENCE: 'INVALID_EXPERIENCE',
|
||||
INVALID_GENDER: 'INVALID_GENDER',
|
||||
ACTOR_CREATION_FAILED: 'ACTOR_CREATION_FAILED',
|
||||
MGT2E_NOT_ACTIVE: 'MGT2E_NOT_ACTIVE',
|
||||
BASE_ACTOR_NOT_FOUND: 'BASE_ACTOR_NOT_FOUND'
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Fonctions utilitaires
|
||||
// ============================================================================
|
||||
@@ -645,7 +843,7 @@ export function popRandomItems(array, count) {
|
||||
/**
|
||||
* Trouve un rôle par sa clé
|
||||
* @param {string} key - Clé du rôle
|
||||
* @returns {Object|null} - Objet rôle ou null
|
||||
* @returns {Object} - Objet rôle
|
||||
*/
|
||||
export function getRoleByKey(key) {
|
||||
const found = ROLE_LIST.find(r => r.key === key);
|
||||
@@ -700,6 +898,42 @@ export function getCharacteristicPrioritiesForRole(roleKey) {
|
||||
return CHARACTERISTIC_PRIORITIES[roleKey] || CHARACTERISTIC_PRIORITIES.pilot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les options de génération
|
||||
* @param {Object} options - Options à valider
|
||||
* @returns {Object} - Options validées
|
||||
*/
|
||||
export function validateOptions(options = {}) {
|
||||
const errors = [];
|
||||
const validated = { ...options };
|
||||
|
||||
if (validated.citizenCategory && !getCitizenCategoryByKey(validated.citizenCategory)) {
|
||||
errors.push(`Catégorie de citoyen invalide: ${validated.citizenCategory}`);
|
||||
validated.citizenCategory = DEFAULT_OPTIONS.citizenCategory;
|
||||
}
|
||||
|
||||
if (validated.experience && !getExperienceLevelByKey(validated.experience)) {
|
||||
errors.push(`Niveau d'expérience invalide: ${validated.experience}`);
|
||||
validated.experience = DEFAULT_OPTIONS.experience;
|
||||
}
|
||||
|
||||
if (validated.role && !getRoleByKey(validated.role)) {
|
||||
errors.push(`Rôle invalide: ${validated.role}`);
|
||||
validated.role = DEFAULT_OPTIONS.role;
|
||||
}
|
||||
|
||||
if (validated.gender && !getGenderByKey(validated.gender)) {
|
||||
errors.push(`Genre invalide: ${validated.gender}`);
|
||||
validated.gender = DEFAULT_OPTIONS.gender;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.warn(`${MODULE_ID} | Options de génération invalides:`, errors);
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Données par défaut
|
||||
// ============================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+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';
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Traveller NPC Generator - Utilitaires
|
||||
*
|
||||
* Ce fichier contient les classes et fonctions utilitaires pour le générateur.
|
||||
*/
|
||||
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
// ============================================================================
|
||||
// Classe de gestion des erreurs
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Erreur spécifique au générateur de PNJ Traveller
|
||||
*/
|
||||
export class TravellerNpcError extends Error {
|
||||
/**
|
||||
* @param {string} message - Message d'erreur
|
||||
* @param {string} code - Code d'erreur
|
||||
* @param {Object} [details={}] - Détails supplémentaires
|
||||
*/
|
||||
constructor(message, code, details = {}) {
|
||||
super(message);
|
||||
this.name = 'TravellerNpcError';
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
this.isTravellerNpcError = true;
|
||||
this.timestamp = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une erreur à partir d'une erreur existante
|
||||
* @param {Error} error - Erreur originale
|
||||
* @param {string} code - Code d'erreur
|
||||
* @returns {TravellerNpcError}
|
||||
*/
|
||||
static from(error, code = 'UNKNOWN') {
|
||||
if (error?.isTravellerNpcError) {
|
||||
return error;
|
||||
}
|
||||
return new TravellerNpcError(
|
||||
error?.message || 'Erreur inconnue',
|
||||
code,
|
||||
{ originalError: error }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log l'erreur dans la console
|
||||
*/
|
||||
log() {
|
||||
console.error(`${MODULE_ID} | [${this.code}] ${this.message}`, {
|
||||
details: this.details,
|
||||
timestamp: this.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche une notification à l'utilisateur et log l'erreur
|
||||
*/
|
||||
notify() {
|
||||
ui.notifications?.error(`${this.code}: ${this.message}`);
|
||||
this.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une notification sans lancer d'erreur
|
||||
* @param {string} message - Message
|
||||
* @param {string} code - Code
|
||||
*/
|
||||
static warn(message, code) {
|
||||
const error = new TravellerNpcError(message, code);
|
||||
error.log();
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classe de cache pour le module
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Système de cache générique pour le module
|
||||
*/
|
||||
export class ModuleCache {
|
||||
/**
|
||||
* @param {string} moduleId - ID du module
|
||||
* @param {number} [defaultTTL=300000] - Durée de vie par défaut (5 min)
|
||||
*/
|
||||
constructor(moduleId, defaultTTL = 300000) {
|
||||
this.moduleId = moduleId;
|
||||
this.defaultTTL = defaultTTL;
|
||||
this.cache = new Map();
|
||||
this.pending = new Map();
|
||||
this.timestamps = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une valeur du cache ou la fetch
|
||||
* @template T
|
||||
* @param {string} key - Clé de cache
|
||||
* @param {Function} fetchFn - Fonction de récupération
|
||||
* @param {Object} [options={}] - Options
|
||||
* @param {boolean} [options.forceRefresh=false] - Forcer le rafraîchissement
|
||||
* @param {number} [options.ttl] - TTL spécifique
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async getOrFetch(key, fetchFn, options = {}) {
|
||||
const { forceRefresh = false, ttl = this.defaultTTL } = options;
|
||||
|
||||
// Si déjà en cache et non expiré
|
||||
if (!forceRefresh && this.cache.has(key)) {
|
||||
const timestamp = this.timestamps.get(key);
|
||||
if (Date.now() - timestamp < ttl) {
|
||||
return foundry.utils.deepClone(this.cache.get(key));
|
||||
}
|
||||
// Expiré, on le supprime
|
||||
this.clear(key);
|
||||
}
|
||||
|
||||
// Si déjà en cours de fetch pour cette clé
|
||||
if (this.pending.has(key)) {
|
||||
return this.pending.get(key);
|
||||
}
|
||||
|
||||
// Nouveau fetch
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const result = await fetchFn();
|
||||
const cachedResult = result ? foundry.utils.deepClone(result) : null;
|
||||
this.cache.set(key, cachedResult);
|
||||
this.timestamps.set(key, Date.now());
|
||||
this.pending.delete(key);
|
||||
return foundry.utils.deepClone(cachedResult);
|
||||
} catch (error) {
|
||||
console.warn(`${this.moduleId} | Erreur de cache pour ${key}:`, error);
|
||||
this.pending.delete(key);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
this.pending.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide une entrée du cache
|
||||
* @param {string} key - Clé à supprimer
|
||||
*/
|
||||
clear(key) {
|
||||
this.cache.delete(key);
|
||||
this.pending.delete(key);
|
||||
this.timestamps.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide tout le cache
|
||||
*/
|
||||
clearAll() {
|
||||
this.cache.clear();
|
||||
this.pending.clear();
|
||||
this.timestamps.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une clé est en cache
|
||||
* @param {string} key - Clé à vérifier
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(key) {
|
||||
if (!this.cache.has(key)) return false;
|
||||
const timestamp = this.timestamps.get(key);
|
||||
return Date.now() - timestamp < this.defaultTTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une valeur du cache sans vérification d'expiration
|
||||
* @template T
|
||||
* @param {string} key - Clé
|
||||
* @returns {T|null}
|
||||
*/
|
||||
get(key) {
|
||||
return this.cache.get(key) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Instance de cache pour le module
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Instance de cache partagée pour le générateur de PNJ Traveller
|
||||
* @type {ModuleCache}
|
||||
*/
|
||||
export const travellerNpcCache = new ModuleCache(MODULE_ID, 300000); // 5 min TTL
|
||||
|
||||
// ============================================================================
|
||||
// Codes d'erreur
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Codes d'erreur standard pour le module
|
||||
*/
|
||||
export const ERROR_CODES = {
|
||||
INVALID_OPTIONS: 'INVALID_OPTIONS',
|
||||
INVALID_ROLE: 'INVALID_ROLE',
|
||||
INVALID_CATEGORY: 'INVALID_CATEGORY',
|
||||
INVALID_EXPERIENCE: 'INVALID_EXPERIENCE',
|
||||
INVALID_GENDER: 'INVALID_GENDER',
|
||||
ACTOR_CREATION_FAILED: 'ACTOR_CREATION_FAILED',
|
||||
MGT2E_NOT_ACTIVE: 'MGT2E_NOT_ACTIVE',
|
||||
BASE_ACTOR_NOT_FOUND: 'BASE_ACTOR_NOT_FOUND',
|
||||
CACHE_ERROR: 'CACHE_ERROR',
|
||||
GENERATION_ERROR: 'GENERATION_ERROR'
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Fonctions utilitaires générales
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Formate un message de debug
|
||||
* @param {string} message - Message
|
||||
* @param {Object} [data={}] - Données supplémentaires
|
||||
*/
|
||||
export function debug(message, data = {}) {
|
||||
if (game.settings.get(MODULE_ID, 'debug') || game.user?.isGM) {
|
||||
console.debug(`${MODULE_ID} | ${message}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un message de log
|
||||
* @param {string} message - Message
|
||||
* @param {Object} [data={}] - Données supplémentaires
|
||||
*/
|
||||
export function log(message, data = {}) {
|
||||
console.log(`${MODULE_ID} | ${message}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un message d'avertissement
|
||||
* @param {string} message - Message
|
||||
* @param {Object} [data={}] - Données supplémentaires
|
||||
*/
|
||||
export function warn(message, data = {}) {
|
||||
console.warn(`${MODULE_ID} | ${message}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un message d'erreur
|
||||
* @param {string} message - Message
|
||||
* @param {Object} [data={}] - Données supplémentaires
|
||||
*/
|
||||
export function error(message, data = {}) {
|
||||
console.error(`${MODULE_ID} | ${message}`, data);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export par défaut
|
||||
// ============================================================================
|
||||
|
||||
export default {
|
||||
TravellerNpcError,
|
||||
ModuleCache,
|
||||
travellerNpcCache,
|
||||
ERROR_CODES,
|
||||
debug,
|
||||
log,
|
||||
warn,
|
||||
error
|
||||
};
|
||||
+407
-124
@@ -1,88 +1,145 @@
|
||||
/**
|
||||
* Styles pour le générateur de PNJ Traveller
|
||||
* Aligné avec les styles des dialogues /commerce et /pnj du module
|
||||
* Compatible avec Foundry VTT v13 et v14
|
||||
*/
|
||||
|
||||
/* Conteneur principal */
|
||||
.mgt2-traveller-npc-dialog {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 0 20px rgba(217, 178, 76, 0.3);
|
||||
}
|
||||
/* ==========================================================================
|
||||
Conteneur principal - Aligné sur mgt2-npc-dialog et mgt2-commerce-dialog
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-dialog .window-content {
|
||||
background: rgba(10, 10, 10, 0.9);
|
||||
color: #d8c79a;
|
||||
}
|
||||
|
||||
/* Formulaire */
|
||||
.mgt2-traveller-npc-form {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form h3 {
|
||||
color: #d9b24c;
|
||||
.mgt2-traveller-npc-dialog .window-header {
|
||||
background: linear-gradient(180deg, rgba(44, 44, 62, 0.96) 0%, rgba(30, 30, 43, 0.96) 100%);
|
||||
border-bottom: 1px solid #c9a227;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-dialog .window-title {
|
||||
color: #d9b24c;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-dialog .window-content,
|
||||
#mgt2-traveller-npc .window-content {
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
background: #f5f0e8;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Formulaire principal - Aligné sur mgt2-npc-form et mgt2-commerce-form
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f0e8;
|
||||
color: #222;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form h3,
|
||||
.mgt2-npc-dialog .mgt2-traveller-npc-form h3 {
|
||||
margin: 0 0 12px;
|
||||
color: #5f4300 !important;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #b78f26 !important;
|
||||
padding-bottom: 5px;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form h3 i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .traveller-npc-intro {
|
||||
color: #a0a0a0;
|
||||
margin-bottom: 20px;
|
||||
margin: 0 0 10px;
|
||||
color: #555;
|
||||
font-size: 0.87em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Champs de formulaire */
|
||||
/* ==========================================================================
|
||||
Champs de formulaire - Aligné sur mgt2-npc-form
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form .form-group {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .form-group-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .form-group-row .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #d8c79a;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: #444;
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form select,
|
||||
.mgt2-traveller-npc-form input[type="text"] {
|
||||
.mgt2-traveller-npc-form input[type="text"],
|
||||
.mgt2-traveller-npc-form select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid #444;
|
||||
box-sizing: border-box;
|
||||
padding: 5px 7px;
|
||||
font-size: 0.85em;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form select:focus,
|
||||
.mgt2-traveller-npc-form input[type="text"]:focus {
|
||||
.mgt2-traveller-npc-form input[type="text"]:focus,
|
||||
.mgt2-traveller-npc-form select:focus {
|
||||
border-color: #c9a227;
|
||||
outline: none;
|
||||
border-color: #d9b24c;
|
||||
box-shadow: 0 0 0 2px rgba(201, 162, 39, 0.22);
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form select option {
|
||||
background: #1a1a1a;
|
||||
color: #ffffff;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
/* Champs de nom */
|
||||
/* ==========================================================================
|
||||
Fieldset - Aligné sur mgt2-npc-form
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form fieldset {
|
||||
border: 1px solid #c9a227;
|
||||
border-radius: 5px;
|
||||
padding: 10px 12px 8px;
|
||||
margin: 10px 0;
|
||||
background: rgba(201, 162, 39, 0.04);
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-dialog .mgt2-traveller-npc-form legend,
|
||||
.mgt2-traveller-npc-form legend {
|
||||
color: #7a5c00 !important;
|
||||
font-size: 0.78em;
|
||||
font-weight: bold;
|
||||
padding: 0 5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Champs de nom
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form .name-fields {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -98,20 +155,26 @@
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .name-fields .btn-small {
|
||||
padding: 6px 10px;
|
||||
background: rgba(217, 178, 76, 0.3);
|
||||
padding: 5px 10px;
|
||||
background: #2c2c3e;
|
||||
color: #e1bc57;
|
||||
border: 1px solid #c9a227;
|
||||
color: #ffffff;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
min-width: 36px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .name-fields .btn-small:hover {
|
||||
background: rgba(217, 178, 76, 0.5);
|
||||
background: #243852;
|
||||
color: #f2d27a;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
/* ==========================================================================
|
||||
Checkbox
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form .checkbox-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -119,79 +182,101 @@
|
||||
.mgt2-traveller-npc-form .checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 0.85em;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .checkbox-group input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
width: auto;
|
||||
accent-color: #c9a227;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hint */
|
||||
/* ==========================================================================
|
||||
Hint
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form .hint {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
font-weight: normal;
|
||||
font-size: 0.85em;
|
||||
color: #777;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Pied de formulaire */
|
||||
.mgt2-traveller-npc-form .form-footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
/* ==========================================================================
|
||||
Required field indicator
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form .required {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Pied de formulaire - Aligné sur mgt2-npc-form
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-form .form-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
button.btn-calculate,
|
||||
.mgt2-traveller-npc-form .btn-calculate {
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #c9a227, #d9b24c);
|
||||
border: none;
|
||||
background: #2c2c3e;
|
||||
color: #e1bc57;
|
||||
border: 1px solid #c9a227;
|
||||
border-radius: 4px;
|
||||
color: #000;
|
||||
padding: 7px 18px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
button.btn-calculate:hover,
|
||||
.mgt2-traveller-npc-form .btn-calculate:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(217, 178, 76, 0.4);
|
||||
background: #243852;
|
||||
color: #f2d27a;
|
||||
}
|
||||
|
||||
button.btn-calculate:disabled,
|
||||
.mgt2-traveller-npc-form .btn-calculate:disabled {
|
||||
opacity: 0.7;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .btn-calculate i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Required field indicator */
|
||||
.mgt2-traveller-npc-form .required {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
/* ==========================================================================
|
||||
Résultat - Aligné sur mgt2-npc-result
|
||||
======================================================================== */
|
||||
|
||||
/* Resultat */
|
||||
.mgt2-npc-result,
|
||||
.traveller-npc-result {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
font-size: 0.85em;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #333;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #c9a227;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-header h3 {
|
||||
color: #d9b24c;
|
||||
color: #5f4300 !important;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1em;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-header h3 i {
|
||||
@@ -199,22 +284,23 @@
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-name {
|
||||
font-size: 18px;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-notice {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-notice.success {
|
||||
background: rgba(46, 204, 113, 0.2);
|
||||
border: 1px solid #2ecc71;
|
||||
color: #2ecc71;
|
||||
background: #eef8ee;
|
||||
border: 1px solid #a9d0a9;
|
||||
color: #2a6a2a;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-notice.success i {
|
||||
@@ -223,37 +309,45 @@
|
||||
|
||||
.traveller-npc-result .npc-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-detail {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fbf8f1;
|
||||
padding: 6px 8px;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
border: 1px solid #d7ccb0;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-detail-label {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.75em;
|
||||
color: #7a5c00;
|
||||
margin-bottom: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-detail-value {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
color: #222;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-section h4 {
|
||||
color: #c9a227;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
color: #5f4300 !important;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid #d7ccb0;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-section h4 i {
|
||||
@@ -263,69 +357,258 @@
|
||||
.traveller-npc-result .npc-characteristics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-characteristic {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
min-width: 80px;
|
||||
background: #fbf8f1;
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
border: 1px solid #d7ccb0;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-char-key {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
font-size: 0.7em;
|
||||
color: #7a5c00;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-char-value {
|
||||
font-size: 16px;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-char-dm {
|
||||
font-size: 10px;
|
||||
font-size: 0.7em;
|
||||
color: #c9a227;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-skills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-skill {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #fbf8f1;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
font-size: 0.85em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid #d7ccb0;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-skill-name {
|
||||
color: #ffffff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-skill-level {
|
||||
color: #d9b24c;
|
||||
color: #c9a227;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-footer {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #333;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #d7ccb0;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Niveaux de compétence */
|
||||
/* ==========================================================================
|
||||
Niveaux de compétence
|
||||
======================================================================== */
|
||||
|
||||
.traveller-npc-result .skillLevelSymbol {
|
||||
font-size: 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Onglets personnalisés pour le dialogue
|
||||
======================================================================== */
|
||||
|
||||
.mgt2-traveller-npc-dialog .mgt2-traveller-npc-form .tabs {
|
||||
display: flex;
|
||||
background: #2c2c3e;
|
||||
border-bottom: 3px solid #c9a227;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-dialog .mgt2-traveller-npc-form .tabs .item {
|
||||
flex: 1;
|
||||
padding: 9px 8px;
|
||||
text-align: center;
|
||||
color: #d8c79a !important;
|
||||
font-size: 0.82em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -3px;
|
||||
transition: color 0.18s, border-color 0.18s, background 0.18s;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-dialog .mgt2-traveller-npc-form .tabs .item:hover {
|
||||
color: #f3e3b1 !important;
|
||||
background: rgba(201, 162, 39, 0.16) !important;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-dialog .mgt2-traveller-npc-form .tabs .item.active {
|
||||
color: #d9b24c !important;
|
||||
border-bottom-color: #c9a227 !important;
|
||||
background: rgba(201, 162, 39, 0.18) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Accessibilité
|
||||
======================================================================== */
|
||||
|
||||
/* Focus visible pour la navigation clavier */
|
||||
.mgt2-traveller-npc-form select:focus-visible,
|
||||
.mgt2-traveller-npc-form input:focus-visible,
|
||||
.mgt2-traveller-npc-form button:focus-visible {
|
||||
outline: 2px solid #c9a227;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Contraste amélioré pour l'accessibilité */
|
||||
@media (prefers-contrast: high) {
|
||||
.mgt2-traveller-npc-form label {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form input,
|
||||
.mgt2-traveller-npc-form select {
|
||||
background: #fff;
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Design réactif
|
||||
======================================================================== */
|
||||
|
||||
/* Écran large */
|
||||
@media (min-width: 900px) {
|
||||
.mgt2-traveller-npc-dialog {
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .form-group-row {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-details-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Écran moyen */
|
||||
@media (max-width: 899px) {
|
||||
.mgt2-traveller-npc-dialog {
|
||||
width: 90vw;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-details-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 600px) {
|
||||
.mgt2-traveller-npc-dialog {
|
||||
width: 95vw;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .form-group-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form .name-fields {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-characteristics {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-characteristic {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-skills {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Très petit écran */
|
||||
@media (max-width: 400px) {
|
||||
.mgt2-traveller-npc-form .btn-calculate {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Impression
|
||||
======================================================================== */
|
||||
|
||||
@media print {
|
||||
.mgt2-traveller-npc-dialog .window-header,
|
||||
.mgt2-traveller-npc-dialog .window-content {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form {
|
||||
color: black;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.mgt2-traveller-npc-form input,
|
||||
.mgt2-traveller-npc-form select {
|
||||
background: white;
|
||||
color: black;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.traveller-npc-result {
|
||||
background: white;
|
||||
color: black;
|
||||
border: 1px solid #ccc;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.traveller-npc-result .npc-detail,
|
||||
.traveller-npc-result .npc-characteristic,
|
||||
.traveller-npc-result .npc-skill {
|
||||
background: #f9f9f9;
|
||||
border-color: #ccc;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
<div class="npc-skills">
|
||||
{{#each skills}}
|
||||
{{#if (gt level 0)}}
|
||||
<div class="npc-skill {{skillLevelClass level}}">
|
||||
<span class="npc-skill-name">{{name}}-{{level}}</span>
|
||||
<div class="npc-skill {{skillLevelClass level}}" title="{{labelFr}}">
|
||||
<span class="npc-skill-name">{{labelFr}} {{level}}</span>
|
||||
<span class="npc-skill-level">{{skillLevelSymbol level}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
Reference in New Issue
Block a user