7 Commits

Author SHA1 Message Date
uberwald 9d96ec5543 Fix: Localisation des compétences du PNJ rapide dans le chat
- Ajout des helpers Handlebars localizeSkill et joinLocalizedSkills
- Template npc-result.hbs utilise maintenant joinLocalizedSkills pour afficher les compétences en français
- Import de localizeSkill depuis mgt2eSkills.js
- Résout le problème où les compétences s'affichaient en anglais (pilot, guncombat, melee...) au lieu de français (Pilote, Combat Arme, Mêlée...)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-28 01:06:25 +02:00
uberwald b6a4148829 Fix: Correction complète des mappings de compétences Traveller → mgt2e
- Utilisation des clés internes EXACTES du système mgt2e (basé sur fr.json)
- Pilot-Spacecraft → pilot.spacecraft (au lieu de pilot.vaisseaux_spatiaux)
- Pilot-Small Craft → pilot.smallCraft (au lieu de pilot.petits_vaisseaux)
- Electronics-Sensors → electronics.sensors (au lieu de electronics.capteurs)
- Gunner-Turrets → gunner.turret (au lieu de guncombat.tourelles)
- Engineer-MDrive → engineer.mDrive (au lieu de engineer.propulsion_manoeuvre)
- Athletics-Strength → athletics.strength (au lieu de athletics.force)
- Art-Acting → art.performer (au lieu de art.jeu_acteur)
- Toutes les spécialités utilisent maintenant le format camelCase sans accents

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-28 01:02:41 +02:00
uberwald d8c61458ea Fix: Activation des spécialités dans les fiches PNJ Traveller
- Correction de setSkillLevel pour créer les spécialités manquantes automatiquement
- Mapping corrigé: Pilot-Spacecraft -> pilot.vaisseaux_spatiaux (pluriel)
- Résout le problème où les spécialités comme 'Vaisseaux spatiaux' n'étaient pas activées

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-28 00:53:20 +02:00
uberwald 9453c15d58 Mise à jour des compendiums et scripts pour v14
- Mise à jour des manifestes et logs des packs
- Modification des scripts NPC (NpcDialog.js, travellerNpcGenerator.js, npc.js)
- Mise à jour de la description du module pour refléter l'onglet 'PNJ Détaillé'

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-28 00:47:11 +02:00
uberwald 76870c27bf Corrections, styles et traductions FR 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

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

Vérification des compétences:
 Pilot: 13 compétences (avant: 12, perdait Pilot-Small Craft)
 Engineer: 13 compétences (avant: 10, perdait 3 spécialisations)
 Tous les rôles conservent leurs spécialisations

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>
2026-05-28 00:03:44 +02:00
uberwald ef7fe6e2bd 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>
2026-05-27 23:56:21 +02:00
uberwald 4f53d903eb 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>
2026-05-27 23:09:43 +02:00
95 changed files with 4625 additions and 333 deletions
+12 -4
View File
@@ -1,20 +1,28 @@
{ {
"id": "mgt2-compendium-amiral-denisov", "id": "mgt2-compendium-amiral-denisov",
"title": "MgT2e - Compendium Amiral Denisov", "title": "MgT2e - Compendium Amiral Denisov",
"version": "1.2.1", "version": "1.3.0",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13", "verified": "13",
"maximum": "14" "maximum": "14"
}, },
"description": "Module de compendium et d'outils Mongoose Traveller 2e pour FoundryVTT écrit par JdR.Ninja.\nInclut les commandes /commerce, /pnj, /rencontre et /mission pour automatiser le commerce, les PNJ rapides, les rencontres et les contrats aléatoires au-dessus du système mgt2e, en s'appuyant sur les compétences natives des fiches.", "description": "Module de compendium et d'outils Mongoose Traveller 2e pour FoundryVTT écrit par JdR.Ninja.\nInclut les commandes /commerce, /pnj, /rencontre et /mission pour automatiser le commerce, les PNJ rapides, les rencontres et les contrats aléatoires. La fenêtre /pnj inclut un onglet 'PNJ Détaillé' pour la génération de PNJ Traveller selon les règles du générateur officiel, en s'appuyant sur les compétences natives des fiches.",
"esmodules": [ "esmodules": [
"scripts/commerce.js", "scripts/commerce.js",
"scripts/npc.js" "scripts/npc.js",
"scripts/utils/travellerNpcUtils.js",
"scripts/data/travellerNpcGenerator.js",
"scripts/travellerNpcGenerator.js",
"scripts/TravellerNpcDialog.js",
"scripts/mgt2eMigration.js",
"scripts/npcRollTableSync.js",
"scripts/mgt2eSkills.js"
], ],
"styles": [ "styles": [
"styles/commerce.css", "styles/commerce.css",
"styles/npc.css" "styles/npc.css",
"styles/traveller-npc.css"
], ],
"packs": [ "packs": [
{ {
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000075 MANIFEST-000084
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.659414 7fdf5b7fe6c0 Recovering log #73 2026/05/27-23:11:58.414164 7f3e6f7fe6c0 Recovering log #82
2026/05/24-16:50:54.670747 7fdf5b7fe6c0 Delete type=3 #71 2026/05/27-23:11:58.424222 7f3e6f7fe6c0 Delete type=3 #80
2026/05/24-16:50:54.670783 7fdf5b7fe6c0 Delete type=0 #73 2026/05/27-23:11:58.424274 7f3e6f7fe6c0 Delete type=0 #82
2026/05/24-17:30:55.121088 7fdf5affd6c0 Level-0 table #78: started
2026/05/24-17:30:55.125303 7fdf5affd6c0 Level-0 table #78: 9864 bytes OK
2026/05/24-17:30:55.131248 7fdf5affd6c0 Delete type=0 #76
2026/05/24-17:30:55.131416 7fdf5affd6c0 Manual compaction at level-0 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.131452 7fdf5affd6c0 Manual compaction at level-1 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at '!items!yoIqL0RQEnzNVJB6' @ 161 : 1
2026/05/24-17:30:55.131457 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.134712 7fdf5affd6c0 Generated table #79@1: 29 keys, 10438 bytes
2026/05/24-17:30:55.134726 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 10438 bytes
2026/05/24-17:30:55.141487 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.141544 7fdf5affd6c0 Delete type=2 #70
2026/05/24-17:30:55.141639 7fdf5affd6c0 Delete type=2 #78
2026/05/24-17:30:55.187693 7fdf5affd6c0 Manual compaction at level-1 from '!items!yoIqL0RQEnzNVJB6' @ 161 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.998100 7f5a46ffd6c0 Recovering log #68 2026/05/27-14:31:25.639035 7f3e6f7fe6c0 Recovering log #77
2026/05/18-20:28:00.055719 7f5a46ffd6c0 Delete type=3 #66 2026/05/27-14:31:25.648169 7f3e6f7fe6c0 Delete type=3 #75
2026/05/18-20:28:00.055824 7f5a46ffd6c0 Delete type=0 #68 2026/05/27-14:31:25.648196 7f3e6f7fe6c0 Delete type=0 #77
2026/05/18-20:29:47.499128 7f5a467fc6c0 Level-0 table #74: started 2026/05/27-14:33:21.786747 7f3e6effd6c0 Level-0 table #83: started
2026/05/18-20:29:47.499199 7f5a467fc6c0 Level-0 table #74: 0 bytes OK 2026/05/27-14:33:21.786766 7f3e6effd6c0 Level-0 table #83: 0 bytes OK
2026/05/18-20:29:47.505640 7f5a467fc6c0 Delete type=0 #72 2026/05/27-14:33:21.793932 7f3e6effd6c0 Delete type=0 #81
2026/05/18-20:29:47.505916 7f5a467fc6c0 Manual compaction at level-0 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.800554 7f3e6effd6c0 Manual compaction at level-0 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.505957 7f5a467fc6c0 Manual compaction at level-1 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.800579 7f3e6effd6c0 Manual compaction at level-1 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000174 MANIFEST-000183
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.546518 7fdf5b7fe6c0 Recovering log #172 2026/05/27-23:11:58.290859 7f3e6f7fe6c0 Recovering log #181
2026/05/24-16:50:54.556572 7fdf5b7fe6c0 Delete type=3 #170 2026/05/27-23:11:58.307573 7f3e6f7fe6c0 Delete type=3 #179
2026/05/24-16:50:54.556614 7fdf5b7fe6c0 Delete type=0 #172 2026/05/27-23:11:58.307626 7f3e6f7fe6c0 Delete type=0 #181
2026/05/24-17:30:54.977322 7fdf5affd6c0 Level-0 table #177: started
2026/05/24-17:30:54.980526 7fdf5affd6c0 Level-0 table #177: 3210 bytes OK
2026/05/24-17:30:54.986935 7fdf5affd6c0 Delete type=0 #175
2026/05/24-17:30:55.016293 7fdf5affd6c0 Manual compaction at level-0 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.016335 7fdf5affd6c0 Manual compaction at level-1 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at '!items!wpBopoosZiWXjlKD' @ 78 : 1
2026/05/24-17:30:55.016340 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.019483 7fdf5affd6c0 Generated table #178@1: 6 keys, 3210 bytes
2026/05/24-17:30:55.019496 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 3210 bytes
2026/05/24-17:30:55.026070 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.026126 7fdf5affd6c0 Delete type=2 #169
2026/05/24-17:30:55.026250 7fdf5affd6c0 Delete type=2 #177
2026/05/24-17:30:55.052180 7fdf5affd6c0 Manual compaction at level-1 from '!items!wpBopoosZiWXjlKD' @ 78 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.309876 7f5a477fe6c0 Recovering log #167 2026/05/27-14:31:25.531736 7f3ebd7ff6c0 Recovering log #176
2026/05/18-20:27:59.371762 7f5a477fe6c0 Delete type=3 #165 2026/05/27-14:31:25.541455 7f3ebd7ff6c0 Delete type=3 #174
2026/05/18-20:27:59.371874 7f5a477fe6c0 Delete type=0 #167 2026/05/27-14:31:25.541507 7f3ebd7ff6c0 Delete type=0 #176
2026/05/18-20:29:47.416279 7f5a467fc6c0 Level-0 table #173: started 2026/05/27-14:33:21.722465 7f3e6effd6c0 Level-0 table #182: started
2026/05/18-20:29:47.416343 7f5a467fc6c0 Level-0 table #173: 0 bytes OK 2026/05/27-14:33:21.722495 7f3e6effd6c0 Level-0 table #182: 0 bytes OK
2026/05/18-20:29:47.425598 7f5a467fc6c0 Delete type=0 #171 2026/05/27-14:33:21.728923 7f3e6effd6c0 Delete type=0 #180
2026/05/18-20:29:47.425873 7f5a467fc6c0 Manual compaction at level-0 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.735721 7f3e6effd6c0 Manual compaction at level-0 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.425920 7f5a467fc6c0 Manual compaction at level-1 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.735741 7f3e6effd6c0 Manual compaction at level-1 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000058 MANIFEST-000067
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.686699 7fdf5bfff6c0 Recovering log #56 2026/05/27-23:11:58.442577 7f3e6f7fe6c0 Recovering log #65
2026/05/24-16:50:54.695982 7fdf5bfff6c0 Delete type=3 #54 2026/05/27-23:11:58.452097 7f3e6f7fe6c0 Delete type=3 #63
2026/05/24-16:50:54.696016 7fdf5bfff6c0 Delete type=0 #56 2026/05/27-23:11:58.452144 7f3e6f7fe6c0 Delete type=0 #65
2026/05/24-17:30:55.150957 7fdf5affd6c0 Level-0 table #61: started
2026/05/24-17:30:55.154345 7fdf5affd6c0 Level-0 table #61: 4273 bytes OK
2026/05/24-17:30:55.160772 7fdf5affd6c0 Delete type=0 #59
2026/05/24-17:30:55.187706 7fdf5affd6c0 Manual compaction at level-0 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.210353 7fdf5affd6c0 Manual compaction at level-1 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at '!items!vJInnoigCTJzuY2S' @ 126 : 1
2026/05/24-17:30:55.210359 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.213753 7fdf5affd6c0 Generated table #62@1: 16 keys, 4273 bytes
2026/05/24-17:30:55.213776 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 4273 bytes
2026/05/24-17:30:55.220661 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.220738 7fdf5affd6c0 Delete type=2 #53
2026/05/24-17:30:55.220857 7fdf5affd6c0 Delete type=2 #61
2026/05/24-17:30:55.237555 7fdf5affd6c0 Manual compaction at level-1 from '!items!vJInnoigCTJzuY2S' @ 126 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:28:00.170623 7f5a477fe6c0 Recovering log #51 2026/05/27-14:31:25.663213 7f3e6f7fe6c0 Recovering log #60
2026/05/18-20:28:00.231017 7f5a477fe6c0 Delete type=3 #49 2026/05/27-14:31:25.672282 7f3e6f7fe6c0 Delete type=3 #58
2026/05/18-20:28:00.231120 7f5a477fe6c0 Delete type=0 #51 2026/05/27-14:31:25.672315 7f3e6f7fe6c0 Delete type=0 #60
2026/05/18-20:29:47.515216 7f5a467fc6c0 Level-0 table #57: started 2026/05/27-14:33:21.794049 7f3e6effd6c0 Level-0 table #66: started
2026/05/18-20:29:47.515280 7f5a467fc6c0 Level-0 table #57: 0 bytes OK 2026/05/27-14:33:21.794078 7f3e6effd6c0 Level-0 table #66: 0 bytes OK
2026/05/18-20:29:47.522604 7f5a467fc6c0 Delete type=0 #55 2026/05/27-14:33:21.800426 7f3e6effd6c0 Delete type=0 #64
2026/05/18-20:29:47.522854 7f5a467fc6c0 Manual compaction at level-0 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.808266 7f3e6effd6c0 Manual compaction at level-0 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.522882 7f5a467fc6c0 Manual compaction at level-1 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.831733 7f3e6effd6c0 Manual compaction at level-1 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000177 MANIFEST-000186
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.559278 7fdf5bfff6c0 Recovering log #175 2026/05/27-23:11:58.309946 7f3ebcffe6c0 Recovering log #184
2026/05/24-16:50:54.569235 7fdf5bfff6c0 Delete type=3 #173 2026/05/27-23:11:58.320486 7f3ebcffe6c0 Delete type=3 #182
2026/05/24-16:50:54.569273 7fdf5bfff6c0 Delete type=0 #175 2026/05/27-23:11:58.320525 7f3ebcffe6c0 Delete type=0 #184
2026/05/24-17:30:54.996524 7fdf5affd6c0 Level-0 table #180: started
2026/05/24-17:30:55.000122 7fdf5affd6c0 Level-0 table #180: 30255 bytes OK
2026/05/24-17:30:55.006363 7fdf5affd6c0 Delete type=0 #178
2026/05/24-17:30:55.016315 7fdf5affd6c0 Manual compaction at level-0 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.041863 7fdf5affd6c0 Manual compaction at level-1 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at '!items!yqjKyTCgpclCuHyK' @ 544 : 1
2026/05/24-17:30:55.041874 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.045796 7fdf5affd6c0 Generated table #181@1: 39 keys, 30255 bytes
2026/05/24-17:30:55.045823 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 30255 bytes
2026/05/24-17:30:55.051904 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.051997 7fdf5affd6c0 Delete type=2 #172
2026/05/24-17:30:55.052110 7fdf5affd6c0 Delete type=2 #180
2026/05/24-17:30:55.062783 7fdf5affd6c0 Manual compaction at level-1 from '!items!yqjKyTCgpclCuHyK' @ 544 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.380990 7f5a47fff6c0 Recovering log #170 2026/05/27-14:31:25.544106 7f3ebd7ff6c0 Recovering log #179
2026/05/18-20:27:59.439837 7f5a47fff6c0 Delete type=3 #168 2026/05/27-14:31:25.554048 7f3ebd7ff6c0 Delete type=3 #177
2026/05/18-20:27:59.439975 7f5a47fff6c0 Delete type=0 #170 2026/05/27-14:31:25.554071 7f3ebd7ff6c0 Delete type=0 #179
2026/05/18-20:29:47.426912 7f5a467fc6c0 Level-0 table #176: started 2026/05/27-14:33:21.729016 7f3e6effd6c0 Level-0 table #185: started
2026/05/18-20:29:47.426974 7f5a467fc6c0 Level-0 table #176: 0 bytes OK 2026/05/27-14:33:21.729040 7f3e6effd6c0 Level-0 table #185: 0 bytes OK
2026/05/18-20:29:47.437378 7f5a467fc6c0 Delete type=0 #174 2026/05/27-14:33:21.735577 7f3e6effd6c0 Delete type=0 #183
2026/05/18-20:29:47.437633 7f5a467fc6c0 Manual compaction at level-0 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.735798 7f3e6effd6c0 Manual compaction at level-0 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.437658 7f5a467fc6c0 Manual compaction at level-1 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.735816 7f3e6effd6c0 Manual compaction at level-1 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000112 MANIFEST-000120
+3 -7
View File
@@ -1,7 +1,3 @@
2026/05/24-16:50:54.623242 7fdf5bfff6c0 Recovering log #110 2026/05/27-23:11:58.375862 7f3e6ffff6c0 Recovering log #118
2026/05/24-16:50:54.633193 7fdf5bfff6c0 Delete type=3 #108 2026/05/27-23:11:58.385912 7f3e6ffff6c0 Delete type=3 #116
2026/05/24-16:50:54.633252 7fdf5bfff6c0 Delete type=0 #110 2026/05/27-23:11:58.385947 7f3e6ffff6c0 Delete type=0 #118
2026/05/24-17:30:55.081463 7fdf5affd6c0 Level-0 table #115: started
2026/05/24-17:30:55.081487 7fdf5affd6c0 Level-0 table #115: 0 bytes OK
2026/05/24-17:30:55.087560 7fdf5affd6c0 Delete type=0 #113
2026/05/24-17:30:55.097957 7fdf5affd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/18-20:27:59.766699 7f5a47fff6c0 Recovering log #106 2026/05/27-14:31:25.604521 7f3ebd7ff6c0 Recovering log #114
2026/05/18-20:27:59.824574 7f5a47fff6c0 Delete type=3 #104 2026/05/27-14:31:25.613630 7f3ebd7ff6c0 Delete type=3 #112
2026/05/18-20:27:59.824666 7f5a47fff6c0 Delete type=0 #106 2026/05/27-14:31:25.613651 7f3ebd7ff6c0 Delete type=0 #114
2026/05/18-20:29:47.472932 7f5a467fc6c0 Level-0 table #111: started 2026/05/27-14:33:21.767043 7f3e6effd6c0 Level-0 table #119: started
2026/05/18-20:29:47.473002 7f5a467fc6c0 Level-0 table #111: 0 bytes OK 2026/05/27-14:33:21.767067 7f3e6effd6c0 Level-0 table #119: 0 bytes OK
2026/05/18-20:29:47.479740 7f5a467fc6c0 Delete type=0 #109 2026/05/27-14:33:21.773608 7f3e6effd6c0 Delete type=0 #117
2026/05/18-20:29:47.479998 7f5a467fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.780095 7f3e6effd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000119 MANIFEST-000128
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.597446 7fdf5b7fe6c0 Recovering log #117 2026/05/27-23:11:58.349736 7f3e6ffff6c0 Recovering log #126
2026/05/24-16:50:54.607335 7fdf5b7fe6c0 Delete type=3 #115 2026/05/27-23:11:58.359183 7f3e6ffff6c0 Delete type=3 #124
2026/05/24-16:50:54.607379 7fdf5b7fe6c0 Delete type=0 #117 2026/05/27-23:11:58.359211 7f3e6ffff6c0 Delete type=0 #126
2026/05/24-17:30:55.052370 7fdf5affd6c0 Level-0 table #122: started
2026/05/24-17:30:55.056355 7fdf5affd6c0 Level-0 table #122: 20248 bytes OK
2026/05/24-17:30:55.062721 7fdf5affd6c0 Delete type=0 #120
2026/05/24-17:30:55.081449 7fdf5affd6c0 Manual compaction at level-0 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.087647 7fdf5affd6c0 Manual compaction at level-1 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at '!items!zRJfxioYBRq4iSBR' @ 258 : 1
2026/05/24-17:30:55.087653 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.091676 7fdf5affd6c0 Generated table #123@1: 42 keys, 19863 bytes
2026/05/24-17:30:55.091690 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 19863 bytes
2026/05/24-17:30:55.097740 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.097812 7fdf5affd6c0 Delete type=2 #114
2026/05/24-17:30:55.097896 7fdf5affd6c0 Delete type=2 #122
2026/05/24-17:30:55.104450 7fdf5affd6c0 Manual compaction at level-1 from '!items!zRJfxioYBRq4iSBR' @ 258 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.596317 7f5a47fff6c0 Recovering log #112 2026/05/27-14:31:25.580201 7f3ebd7ff6c0 Recovering log #121
2026/05/18-20:27:59.649784 7f5a47fff6c0 Delete type=3 #110 2026/05/27-14:31:25.589450 7f3ebd7ff6c0 Delete type=3 #119
2026/05/18-20:27:59.649914 7f5a47fff6c0 Delete type=0 #112 2026/05/27-14:31:25.589494 7f3ebd7ff6c0 Delete type=0 #121
2026/05/18-20:29:47.456954 7f5a467fc6c0 Level-0 table #118: started 2026/05/27-14:33:21.747566 7f3e6effd6c0 Level-0 table #127: started
2026/05/18-20:29:47.457019 7f5a467fc6c0 Level-0 table #118: 0 bytes OK 2026/05/27-14:33:21.747585 7f3e6effd6c0 Level-0 table #127: 0 bytes OK
2026/05/18-20:29:47.463638 7f5a467fc6c0 Delete type=0 #116 2026/05/27-14:33:21.754474 7f3e6effd6c0 Delete type=0 #125
2026/05/18-20:29:47.463868 7f5a467fc6c0 Manual compaction at level-0 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.760899 7f3e6effd6c0 Manual compaction at level-0 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.463950 7f5a467fc6c0 Manual compaction at level-1 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.760915 7f3e6effd6c0 Manual compaction at level-1 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000112 MANIFEST-000120
+3 -7
View File
@@ -1,7 +1,3 @@
2026/05/24-16:50:54.634756 7fdfa8dfe6c0 Recovering log #110 2026/05/27-23:11:58.388239 7f3e6f7fe6c0 Recovering log #118
2026/05/24-16:50:54.645375 7fdfa8dfe6c0 Delete type=3 #108 2026/05/27-23:11:58.398684 7f3e6f7fe6c0 Delete type=3 #116
2026/05/24-16:50:54.645440 7fdfa8dfe6c0 Delete type=0 #110 2026/05/27-23:11:58.398724 7f3e6f7fe6c0 Delete type=0 #118
2026/05/24-17:30:55.097965 7fdf5affd6c0 Level-0 table #115: started
2026/05/24-17:30:55.097985 7fdf5affd6c0 Level-0 table #115: 0 bytes OK
2026/05/24-17:30:55.104346 7fdf5affd6c0 Delete type=0 #113
2026/05/24-17:30:55.131378 7fdf5affd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/18-20:27:59.838862 7f5a46ffd6c0 Recovering log #106 2026/05/27-14:31:25.615623 7f3ebd7ff6c0 Recovering log #114
2026/05/18-20:27:59.893943 7f5a46ffd6c0 Delete type=3 #104 2026/05/27-14:31:25.625348 7f3ebd7ff6c0 Delete type=3 #112
2026/05/18-20:27:59.894068 7f5a46ffd6c0 Delete type=0 #106 2026/05/27-14:31:25.625369 7f3ebd7ff6c0 Delete type=0 #114
2026/05/18-20:29:47.480953 7f5a467fc6c0 Level-0 table #111: started 2026/05/27-14:33:21.761003 7f3e6effd6c0 Level-0 table #119: started
2026/05/18-20:29:47.481012 7f5a467fc6c0 Level-0 table #111: 0 bytes OK 2026/05/27-14:33:21.761022 7f3e6effd6c0 Level-0 table #119: 0 bytes OK
2026/05/18-20:29:47.487588 7f5a467fc6c0 Delete type=0 #109 2026/05/27-14:33:21.766953 7f3e6effd6c0 Delete type=0 #117
2026/05/18-20:29:47.487817 7f5a467fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.780083 7f3e6effd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000093 MANIFEST-000101
+3 -8
View File
@@ -1,8 +1,3 @@
2026/05/24-16:50:54.647193 7fdf5b7fe6c0 Recovering log #91 2026/05/27-23:11:58.400455 7f3ebd7ff6c0 Recovering log #99
2026/05/24-16:50:54.656827 7fdf5b7fe6c0 Delete type=3 #89 2026/05/27-23:11:58.410587 7f3ebd7ff6c0 Delete type=3 #97
2026/05/24-16:50:54.656870 7fdf5b7fe6c0 Delete type=0 #91 2026/05/27-23:11:58.410645 7f3ebd7ff6c0 Delete type=0 #99
2026/05/24-17:30:55.114778 7fdf5affd6c0 Level-0 table #96: started
2026/05/24-17:30:55.114807 7fdf5affd6c0 Level-0 table #96: 0 bytes OK
2026/05/24-17:30:55.121001 7fdf5affd6c0 Delete type=0 #94
2026/05/24-17:30:55.131407 7fdf5affd6c0 Manual compaction at level-0 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.131437 7fdf5affd6c0 Manual compaction at level-1 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.909714 7f5a477fe6c0 Recovering log #87 2026/05/27-14:31:25.627132 7f3ebd7ff6c0 Recovering log #95
2026/05/18-20:27:59.963841 7f5a477fe6c0 Delete type=3 #85 2026/05/27-14:31:25.636967 7f3ebd7ff6c0 Delete type=3 #93
2026/05/18-20:27:59.963963 7f5a477fe6c0 Delete type=0 #87 2026/05/27-14:31:25.637002 7f3ebd7ff6c0 Delete type=0 #95
2026/05/18-20:29:47.488790 7f5a467fc6c0 Level-0 table #92: started 2026/05/27-14:33:21.773684 7f3e6effd6c0 Level-0 table #100: started
2026/05/18-20:29:47.488852 7f5a467fc6c0 Level-0 table #92: 0 bytes OK 2026/05/27-14:33:21.773700 7f3e6effd6c0 Level-0 table #100: 0 bytes OK
2026/05/18-20:29:47.496720 7f5a467fc6c0 Delete type=0 #90 2026/05/27-14:33:21.779958 7f3e6effd6c0 Delete type=0 #98
2026/05/18-20:29:47.497943 7f5a467fc6c0 Manual compaction at level-0 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.786739 7f3e6effd6c0 Manual compaction at level-0 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.498034 7f5a467fc6c0 Manual compaction at level-1 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.800533 7f3e6effd6c0 Manual compaction at level-1 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000119 MANIFEST-000128
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.573746 7fdf5bfff6c0 Recovering log #117 2026/05/27-23:11:58.323722 7f3e6f7fe6c0 Recovering log #126
2026/05/24-16:50:54.583876 7fdf5bfff6c0 Delete type=3 #115 2026/05/27-23:11:58.334612 7f3e6f7fe6c0 Delete type=3 #124
2026/05/24-16:50:54.583933 7fdf5bfff6c0 Delete type=0 #117 2026/05/27-23:11:58.334668 7f3e6f7fe6c0 Delete type=0 #126
2026/05/24-17:30:55.026316 7fdf5affd6c0 Level-0 table #122: started
2026/05/24-17:30:55.029368 7fdf5affd6c0 Level-0 table #122: 1988 bytes OK
2026/05/24-17:30:55.035351 7fdf5affd6c0 Delete type=0 #120
2026/05/24-17:30:55.052192 7fdf5affd6c0 Manual compaction at level-0 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.062793 7fdf5affd6c0 Manual compaction at level-1 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at '!items!yleVHgRqGoYLvzxT' @ 56 : 1
2026/05/24-17:30:55.062796 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.065855 7fdf5affd6c0 Generated table #123@1: 8 keys, 1988 bytes
2026/05/24-17:30:55.065865 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 1988 bytes
2026/05/24-17:30:55.071780 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.071820 7fdf5affd6c0 Delete type=2 #114
2026/05/24-17:30:55.071877 7fdf5affd6c0 Delete type=2 #122
2026/05/24-17:30:55.087630 7fdf5affd6c0 Manual compaction at level-1 from '!items!yleVHgRqGoYLvzxT' @ 56 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.461881 7f5a46ffd6c0 Recovering log #112 2026/05/27-14:31:25.557570 7f3ebd7ff6c0 Recovering log #121
2026/05/18-20:27:59.517849 7f5a46ffd6c0 Delete type=3 #110 2026/05/27-14:31:25.567130 7f3ebd7ff6c0 Delete type=3 #119
2026/05/18-20:27:59.517983 7f5a46ffd6c0 Delete type=0 #112 2026/05/27-14:31:25.567150 7f3ebd7ff6c0 Delete type=0 #121
2026/05/18-20:29:47.438640 7f5a467fc6c0 Level-0 table #118: started 2026/05/27-14:33:21.735841 7f3e6effd6c0 Level-0 table #127: started
2026/05/18-20:29:47.438693 7f5a467fc6c0 Level-0 table #118: 0 bytes OK 2026/05/27-14:33:21.735866 7f3e6effd6c0 Level-0 table #127: 0 bytes OK
2026/05/18-20:29:47.445247 7f5a467fc6c0 Delete type=0 #116 2026/05/27-14:33:21.741712 7f3e6effd6c0 Delete type=0 #125
2026/05/18-20:29:47.445955 7f5a467fc6c0 Manual compaction at level-0 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.760883 7f3e6effd6c0 Manual compaction at level-0 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.446012 7f5a467fc6c0 Manual compaction at level-1 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.760906 7f3e6effd6c0 Manual compaction at level-1 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000112 MANIFEST-000120
+3 -7
View File
@@ -1,7 +1,3 @@
2026/05/24-16:50:54.586100 7fdfa8dfe6c0 Recovering log #110 2026/05/27-23:11:58.337311 7f3ebcffe6c0 Recovering log #118
2026/05/24-16:50:54.595631 7fdfa8dfe6c0 Delete type=3 #108 2026/05/27-23:11:58.347349 7f3ebcffe6c0 Delete type=3 #116
2026/05/24-16:50:54.595694 7fdfa8dfe6c0 Delete type=0 #110 2026/05/27-23:11:58.347392 7f3ebcffe6c0 Delete type=0 #118
2026/05/24-17:30:55.035470 7fdf5affd6c0 Level-0 table #115: started
2026/05/24-17:30:55.035483 7fdf5affd6c0 Level-0 table #115: 0 bytes OK
2026/05/24-17:30:55.041734 7fdf5affd6c0 Delete type=0 #113
2026/05/24-17:30:55.052200 7fdf5affd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/18-20:27:59.526598 7f5a94bff6c0 Recovering log #106 2026/05/27-14:31:25.569405 7f3ebd7ff6c0 Recovering log #114
2026/05/18-20:27:59.589070 7f5a94bff6c0 Delete type=3 #104 2026/05/27-14:31:25.578632 7f3ebd7ff6c0 Delete type=3 #112
2026/05/18-20:27:59.589252 7f5a94bff6c0 Delete type=0 #106 2026/05/27-14:31:25.578657 7f3ebd7ff6c0 Delete type=0 #114
2026/05/18-20:29:47.448116 7f5a467fc6c0 Level-0 table #111: started 2026/05/27-14:33:21.741780 7f3e6effd6c0 Level-0 table #119: started
2026/05/18-20:29:47.448187 7f5a467fc6c0 Level-0 table #111: 0 bytes OK 2026/05/27-14:33:21.741799 7f3e6effd6c0 Level-0 table #119: 0 bytes OK
2026/05/18-20:29:47.455433 7f5a467fc6c0 Delete type=0 #109 2026/05/27-14:33:21.747507 7f3e6effd6c0 Delete type=0 #117
2026/05/18-20:29:47.455684 7f5a467fc6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.760892 7f3e6effd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000119 MANIFEST-000128
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.610807 7fdfa95ff6c0 Recovering log #117 2026/05/27-23:11:58.362926 7f3ebd7ff6c0 Recovering log #126
2026/05/24-16:50:54.620344 7fdfa95ff6c0 Delete type=3 #115 2026/05/27-23:11:58.372894 7f3ebd7ff6c0 Delete type=3 #124
2026/05/24-16:50:54.620394 7fdfa95ff6c0 Delete type=0 #117 2026/05/27-23:11:58.372934 7f3ebd7ff6c0 Delete type=0 #126
2026/05/24-17:30:55.071908 7fdf5affd6c0 Level-0 table #122: started
2026/05/24-17:30:55.075519 7fdf5affd6c0 Level-0 table #122: 5396 bytes OK
2026/05/24-17:30:55.081331 7fdf5affd6c0 Delete type=0 #120
2026/05/24-17:30:55.097948 7fdf5affd6c0 Manual compaction at level-0 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.104470 7fdf5affd6c0 Manual compaction at level-1 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at '!items!yFvuDyV00NdojxGt' @ 93 : 1
2026/05/24-17:30:55.104477 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.108502 7fdf5affd6c0 Generated table #123@1: 15 keys, 5574 bytes
2026/05/24-17:30:55.108520 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 5574 bytes
2026/05/24-17:30:55.114469 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.114575 7fdf5affd6c0 Delete type=2 #114
2026/05/24-17:30:55.114702 7fdf5affd6c0 Delete type=2 #122
2026/05/24-17:30:55.131396 7fdf5affd6c0 Manual compaction at level-1 from '!items!yFvuDyV00NdojxGt' @ 93 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:27:59.682456 7f5a46ffd6c0 Recovering log #112 2026/05/27-14:31:25.591969 7f3ebd7ff6c0 Recovering log #121
2026/05/18-20:27:59.750017 7f5a46ffd6c0 Delete type=3 #110 2026/05/27-14:31:25.601963 7f3ebd7ff6c0 Delete type=3 #119
2026/05/18-20:27:59.750123 7f5a46ffd6c0 Delete type=0 #112 2026/05/27-14:31:25.601983 7f3ebd7ff6c0 Delete type=0 #121
2026/05/18-20:29:47.464929 7f5a467fc6c0 Level-0 table #118: started 2026/05/27-14:33:21.754559 7f3e6effd6c0 Level-0 table #127: started
2026/05/18-20:29:47.465002 7f5a467fc6c0 Level-0 table #118: 0 bytes OK 2026/05/27-14:33:21.754583 7f3e6effd6c0 Level-0 table #127: 0 bytes OK
2026/05/18-20:29:47.471758 7f5a467fc6c0 Delete type=0 #116 2026/05/27-14:33:21.760810 7f3e6effd6c0 Delete type=0 #125
2026/05/18-20:29:47.472004 7f5a467fc6c0 Manual compaction at level-0 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.760994 7f3e6effd6c0 Manual compaction at level-0 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.472035 7f5a467fc6c0 Manual compaction at level-1 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.773676 7f3e6effd6c0 Manual compaction at level-1 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000021 MANIFEST-000048
+69 -50
View File
@@ -1,50 +1,69 @@
2026/05/24-16:50:54.697724 7fdfa8dfe6c0 Recovering log #18 2026/05/27-23:11:58.454344 7f3ebcffe6c0 Recovering log #44
2026/05/24-16:50:54.707598 7fdfa8dfe6c0 Delete type=3 #16 2026/05/27-23:11:58.463285 7f3ebcffe6c0 Delete type=3 #40
2026/05/24-16:50:54.707634 7fdfa8dfe6c0 Delete type=0 #18 2026/05/27-23:11:58.463300 7f3ebcffe6c0 Delete type=0 #44
2026/05/24-17:00:32.390913 7fdf5affd6c0 Level-0 table #24: started 2026/05/27-23:37:44.587645 7f3e6effd6c0 Level-0 table #51: started
2026/05/24-17:00:32.409568 7fdf5affd6c0 Level-0 table #24: 1225651 bytes OK 2026/05/27-23:37:44.605872 7f3e6effd6c0 Level-0 table #51: 1343154 bytes OK
2026/05/24-17:00:32.416341 7fdf5affd6c0 Delete type=0 #22 2026/05/27-23:37:44.612030 7f3e6effd6c0 Delete type=0 #49
2026/05/24-17:03:56.805332 7fdf5affd6c0 Level-0 table #26: started 2026/05/28-00:04:39.168329 7f3e6effd6c0 Level-0 table #53: started
2026/05/24-17:03:56.824851 7fdf5affd6c0 Level-0 table #26: 1099794 bytes OK 2026/05/28-00:04:39.185209 7f3e6effd6c0 Level-0 table #53: 1393674 bytes OK
2026/05/24-17:03:56.831981 7fdf5affd6c0 Delete type=0 #23 2026/05/28-00:04:39.191603 7f3e6effd6c0 Delete type=0 #50
2026/05/24-17:12:00.516549 7fdf5affd6c0 Level-0 table #28: started 2026/05/28-00:18:55.273857 7f3e6effd6c0 Level-0 table #55: started
2026/05/24-17:12:00.550743 7fdf5affd6c0 Level-0 table #28: 1268131 bytes OK 2026/05/28-00:18:55.294346 7f3e6effd6c0 Level-0 table #55: 1451564 bytes OK
2026/05/24-17:12:00.582730 7fdf5affd6c0 Delete type=0 #25 2026/05/28-00:18:55.300308 7f3e6effd6c0 Delete type=0 #52
2026/05/24-17:16:09.186372 7fdf5affd6c0 Level-0 table #30: started 2026/05/28-00:20:15.021651 7f3e6effd6c0 Level-0 table #57: started
2026/05/24-17:16:09.219830 7fdf5affd6c0 Level-0 table #30: 1432074 bytes OK 2026/05/28-00:20:15.040525 7f3e6effd6c0 Level-0 table #57: 1496314 bytes OK
2026/05/24-17:16:09.258561 7fdf5affd6c0 Delete type=0 #27 2026/05/28-00:20:15.046886 7f3e6effd6c0 Delete type=0 #54
2026/05/24-17:18:30.349891 7fdf5affd6c0 Level-0 table #32: started 2026/05/28-00:20:43.720521 7f3e6effd6c0 Level-0 table #59: started
2026/05/24-17:18:30.379065 7fdf5affd6c0 Level-0 table #32: 1608171 bytes OK 2026/05/28-00:20:43.742455 7f3e6effd6c0 Level-0 table #59: 1546947 bytes OK
2026/05/24-17:18:30.388404 7fdf5affd6c0 Delete type=0 #29 2026/05/28-00:20:43.748835 7f3e6effd6c0 Delete type=0 #56
2026/05/24-17:18:30.388776 7fdf5affd6c0 Compacting 4@0 + 1@1 files 2026/05/28-00:20:43.749387 7f3e6effd6c0 Compacting 4@0 + 1@1 files
2026/05/24-17:18:30.411581 7fdf5affd6c0 Generated table #33@0: 6650 keys, 986248 bytes 2026/05/28-00:20:43.771709 7f3e6effd6c0 Generated table #60@0: 10302 keys, 1546947 bytes
2026/05/24-17:18:30.411611 7fdf5affd6c0 Compacted 4@0 + 1@1 files => 986248 bytes 2026/05/28-00:20:43.771721 7f3e6effd6c0 Compacted 4@0 + 1@1 files => 1546947 bytes
2026/05/24-17:18:30.420963 7fdf5affd6c0 compacted to: files[ 0 1 1 0 0 0 0 ] 2026/05/28-00:20:43.777946 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/24-17:18:30.421191 7fdf5affd6c0 Delete type=2 #24 2026/05/28-00:20:43.778171 7f3e6effd6c0 Delete type=2 #51
2026/05/24-17:18:30.421480 7fdf5affd6c0 Delete type=2 #26 2026/05/28-00:20:43.778444 7f3e6effd6c0 Delete type=2 #53
2026/05/24-17:18:30.421601 7fdf5affd6c0 Delete type=2 #28 2026/05/28-00:20:43.778777 7f3e6effd6c0 Delete type=2 #55
2026/05/24-17:18:30.421786 7fdf5affd6c0 Delete type=2 #30 2026/05/28-00:20:43.778977 7f3e6effd6c0 Delete type=2 #57
2026/05/24-17:18:30.422133 7fdf5affd6c0 Delete type=2 #32 2026/05/28-00:20:43.779160 7f3e6effd6c0 Delete type=2 #59
2026/05/24-17:21:12.979159 7fdf5affd6c0 Level-0 table #35: started 2026/05/28-00:21:51.457842 7f3e6effd6c0 Level-0 table #62: started
2026/05/24-17:21:13.003011 7fdf5affd6c0 Level-0 table #35: 1764850 bytes OK 2026/05/28-00:21:51.479532 7f3e6effd6c0 Level-0 table #62: 1604425 bytes OK
2026/05/24-17:21:13.009270 7fdf5affd6c0 Delete type=0 #31 2026/05/28-00:21:51.485724 7f3e6effd6c0 Delete type=0 #58
2026/05/24-17:30:55.160858 7fdf5affd6c0 Level-0 table #37: started 2026/05/28-00:30:27.802783 7f3e6effd6c0 Level-0 table #64: started
2026/05/24-17:30:55.180790 7fdf5affd6c0 Level-0 table #37: 1929143 bytes OK 2026/05/28-00:30:27.823947 7f3e6effd6c0 Level-0 table #64: 1651065 bytes OK
2026/05/24-17:30:55.187168 7fdf5affd6c0 Delete type=0 #34 2026/05/28-00:30:27.829721 7f3e6effd6c0 Delete type=0 #61
2026/05/24-17:30:55.187713 7fdf5affd6c0 Manual compaction at level-0 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 70890 : 1 2026/05/28-00:35:01.919877 7f3e6effd6c0 Level-0 table #66: started
2026/05/24-17:30:55.187715 7fdf5affd6c0 Compacting 2@0 + 1@1 files 2026/05/28-00:35:01.987012 7f3e6effd6c0 Level-0 table #66: 1703060 bytes OK
2026/05/24-17:30:55.203256 7fdf5affd6c0 Generated table #38@0: 7978 keys, 1186513 bytes 2026/05/28-00:35:02.043369 7f3e6effd6c0 Delete type=0 #63
2026/05/24-17:30:55.203267 7fdf5affd6c0 Compacted 2@0 + 1@1 files => 1186513 bytes 2026/05/28-00:37:26.052331 7f3e6effd6c0 Level-0 table #68: started
2026/05/24-17:30:55.209485 7fdf5affd6c0 compacted to: files[ 0 1 1 0 0 0 0 ] 2026/05/28-00:37:26.076959 7f3e6effd6c0 Level-0 table #68: 1753179 bytes OK
2026/05/24-17:30:55.209584 7fdf5affd6c0 Delete type=2 #33 2026/05/28-00:37:26.083119 7f3e6effd6c0 Delete type=0 #65
2026/05/24-17:30:55.209874 7fdf5affd6c0 Delete type=2 #35 2026/05/28-00:37:26.083369 7f3e6effd6c0 Compacting 4@0 + 1@1 files
2026/05/24-17:30:55.210245 7fdf5affd6c0 Delete type=2 #37 2026/05/28-00:37:26.103213 7f3e6effd6c0 Generated table #69@0: 11630 keys, 1753179 bytes
2026/05/24-17:30:55.237542 7fdf5affd6c0 Manual compaction at level-0 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 70890 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end) 2026/05/28-00:37:26.103227 7f3e6effd6c0 Compacted 4@0 + 1@1 files => 1753179 bytes
2026/05/24-17:30:55.237576 7fdf5affd6c0 Manual compaction at level-1 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 92964 : 1 2026/05/28-00:37:26.109075 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/24-17:30:55.237582 7fdf5affd6c0 Compacting 1@1 + 1@2 files 2026/05/28-00:37:26.109152 7f3e6effd6c0 Delete type=2 #60
2026/05/24-17:30:55.258486 7fdf5affd6c0 Generated table #39@1: 7978 keys, 1186513 bytes 2026/05/28-00:37:26.109336 7f3e6effd6c0 Delete type=2 #62
2026/05/24-17:30:55.258511 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 1186513 bytes 2026/05/28-00:37:26.109453 7f3e6effd6c0 Delete type=2 #64
2026/05/24-17:30:55.264533 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ] 2026/05/28-00:37:26.109722 7f3e6effd6c0 Delete type=2 #66
2026/05/24-17:30:55.264684 7fdf5affd6c0 Delete type=2 #20 2026/05/28-00:37:26.109828 7f3e6effd6c0 Delete type=2 #68
2026/05/24-17:30:55.264949 7fdf5affd6c0 Delete type=2 #38 2026/05/28-00:48:03.393850 7f3e6effd6c0 Level-0 table #71: started
2026/05/24-17:30:55.283063 7fdf5affd6c0 Manual compaction at level-1 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 92964 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end) 2026/05/28-00:48:03.411976 7f3e6effd6c0 Level-0 table #71: 1806724 bytes OK
2026/05/28-00:48:03.419030 7f3e6effd6c0 Delete type=0 #67
2026/05/28-00:52:05.605289 7f3e6effd6c0 Level-0 table #73: started
2026/05/28-00:52:05.629208 7f3e6effd6c0 Level-0 table #73: 1856591 bytes OK
2026/05/28-00:52:05.635297 7f3e6effd6c0 Delete type=0 #70
2026/05/28-00:53:23.184161 7f3e6effd6c0 Level-0 table #75: started
2026/05/28-00:53:23.202618 7f3e6effd6c0 Level-0 table #75: 1901920 bytes OK
2026/05/28-00:53:23.208730 7f3e6effd6c0 Delete type=0 #72
2026/05/28-01:02:57.320650 7f3e6effd6c0 Level-0 table #77: started
2026/05/28-01:02:57.341951 7f3e6effd6c0 Level-0 table #77: 1962797 bytes OK
2026/05/28-01:02:57.347793 7f3e6effd6c0 Delete type=0 #74
2026/05/28-01:02:57.348132 7f3e6effd6c0 Compacting 4@0 + 1@1 files
2026/05/28-01:02:57.369982 7f3e6effd6c0 Generated table #78@0: 12958 keys, 1962797 bytes
2026/05/28-01:02:57.370013 7f3e6effd6c0 Compacted 4@0 + 1@1 files => 1962797 bytes
2026/05/28-01:02:57.376098 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/28-01:02:57.376330 7f3e6effd6c0 Delete type=2 #69
2026/05/28-01:02:57.376716 7f3e6effd6c0 Delete type=2 #71
2026/05/28-01:02:57.377097 7f3e6effd6c0 Delete type=2 #73
2026/05/28-01:02:57.377594 7f3e6effd6c0 Delete type=2 #75
2026/05/28-01:02:57.377833 7f3e6effd6c0 Delete type=2 #77
+25 -15
View File
@@ -1,15 +1,25 @@
2026/05/18-20:28:00.247967 7f5a94bff6c0 Recovering log #13 2026/05/27-14:31:25.674162 7f3e6f7fe6c0 Recovering log #36
2026/05/18-20:28:00.322431 7f5a94bff6c0 Delete type=3 #11 2026/05/27-14:31:25.684284 7f3e6f7fe6c0 Delete type=3 #21
2026/05/18-20:28:00.322531 7f5a94bff6c0 Delete type=0 #13 2026/05/27-14:31:25.684327 7f3e6f7fe6c0 Delete type=0 #36
2026/05/18-20:29:47.524056 7f5a467fc6c0 Level-0 table #19: started 2026/05/27-14:32:13.276652 7f3e6effd6c0 Level-0 table #43: started
2026/05/18-20:29:47.536717 7f5a467fc6c0 Level-0 table #19: 417426 bytes OK 2026/05/27-14:32:13.292995 7f3e6effd6c0 Level-0 table #43: 1244894 bytes OK
2026/05/18-20:29:47.544323 7f5a467fc6c0 Delete type=0 #17 2026/05/27-14:32:13.298790 7f3e6effd6c0 Delete type=0 #41
2026/05/18-20:29:47.545004 7f5a467fc6c0 Manual compaction at level-0 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.808275 7f3e6effd6c0 Level-0 table #45: started
2026/05/18-20:29:47.545055 7f5a467fc6c0 Manual compaction at level-1 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 12474 : 1 2026/05/27-14:33:21.824612 7f3e6effd6c0 Level-0 table #45: 1294793 bytes OK
2026/05/18-20:29:47.545064 7f5a467fc6c0 Compacting 1@1 + 1@2 files 2026/05/27-14:33:21.831380 7f3e6effd6c0 Delete type=0 #42
2026/05/18-20:29:47.560513 7f5a467fc6c0 Generated table #20@1: 2998 keys, 417426 bytes 2026/05/27-14:33:21.838976 7f3e6effd6c0 Manual compaction at level-0 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1
2026/05/18-20:29:47.560554 7f5a467fc6c0 Compacted 1@1 + 1@2 files => 417426 bytes 2026/05/27-14:33:21.838980 7f3e6effd6c0 Compacting 1@0 + 1@1 files
2026/05/18-20:29:47.567200 7f5a467fc6c0 compacted to: files[ 0 0 1 0 0 0 0 ] 2026/05/27-14:33:21.852317 7f3e6effd6c0 Generated table #46@0: 8642 keys, 1294793 bytes
2026/05/18-20:29:47.567433 7f5a467fc6c0 Delete type=2 #15 2026/05/27-14:33:21.852331 7f3e6effd6c0 Compacted 1@0 + 1@1 files => 1294793 bytes
2026/05/18-20:29:47.567841 7f5a467fc6c0 Delete type=2 #19 2026/05/27-14:33:21.859689 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/18-20:29:47.568160 7f5a467fc6c0 Manual compaction at level-1 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 12474 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.859935 7f3e6effd6c0 Delete type=2 #43
2026/05/27-14:33:21.860058 7f3e6effd6c0 Delete type=2 #45
2026/05/27-14:33:21.877111 7f3e6effd6c0 Manual compaction at level-0 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.877144 7f3e6effd6c0 Manual compaction at level-1 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1
2026/05/27-14:33:21.877150 7f3e6effd6c0 Compacting 1@1 + 1@2 files
2026/05/27-14:33:21.895679 7f3e6effd6c0 Generated table #47@1: 8642 keys, 1294793 bytes
2026/05/27-14:33:21.895699 7f3e6effd6c0 Compacted 1@1 + 1@2 files => 1294793 bytes
2026/05/27-14:33:21.901922 7f3e6effd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/27-14:33:21.902059 7f3e6effd6c0 Delete type=2 #39
2026/05/27-14:33:21.902514 7f3e6effd6c0 Delete type=2 #46
2026/05/27-14:33:21.908609 7f3e6effd6c0 Manual compaction at level-1 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000065 MANIFEST-000074
+3 -15
View File
@@ -1,15 +1,3 @@
2026/05/24-16:50:54.673462 7fdfa95ff6c0 Recovering log #63 2026/05/27-23:11:58.427900 7f3ebd7ff6c0 Recovering log #72
2026/05/24-16:50:54.683865 7fdfa95ff6c0 Delete type=3 #61 2026/05/27-23:11:58.438233 7f3ebd7ff6c0 Delete type=3 #70
2026/05/24-16:50:54.683923 7fdfa95ff6c0 Delete type=0 #63 2026/05/27-23:11:58.438292 7f3ebd7ff6c0 Delete type=0 #72
2026/05/24-17:30:55.141699 7fdf5affd6c0 Level-0 table #68: started
2026/05/24-17:30:55.144973 7fdf5affd6c0 Level-0 table #68: 18651 bytes OK
2026/05/24-17:30:55.150867 7fdf5affd6c0 Delete type=0 #66
2026/05/24-17:30:55.187701 7fdf5affd6c0 Manual compaction at level-0 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
2026/05/24-17:30:55.227041 7fdf5affd6c0 Manual compaction at level-1 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at '!items!xFUyR7XECD8QJcIw' @ 231 : 1
2026/05/24-17:30:55.227053 7fdf5affd6c0 Compacting 1@1 + 1@2 files
2026/05/24-17:30:55.230678 7fdf5affd6c0 Generated table #69@1: 33 keys, 18651 bytes
2026/05/24-17:30:55.230696 7fdf5affd6c0 Compacted 1@1 + 1@2 files => 18651 bytes
2026/05/24-17:30:55.237297 7fdf5affd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/24-17:30:55.237359 7fdf5affd6c0 Delete type=2 #60
2026/05/24-17:30:55.237471 7fdf5affd6c0 Delete type=2 #68
2026/05/24-17:30:55.265203 7fdf5affd6c0 Manual compaction at level-1 from '!items!xFUyR7XECD8QJcIw' @ 231 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/18-20:28:00.088714 7f5a94bff6c0 Recovering log #58 2026/05/27-14:31:25.650849 7f3e6f7fe6c0 Recovering log #67
2026/05/18-20:28:00.147839 7f5a94bff6c0 Delete type=3 #56 2026/05/27-14:31:25.660624 7f3e6f7fe6c0 Delete type=3 #65
2026/05/18-20:28:00.147945 7f5a94bff6c0 Delete type=0 #58 2026/05/27-14:31:25.660648 7f3e6f7fe6c0 Delete type=0 #67
2026/05/18-20:29:47.507007 7f5a467fc6c0 Level-0 table #64: started 2026/05/27-14:33:21.780104 7f3e6effd6c0 Level-0 table #73: started
2026/05/18-20:29:47.507077 7f5a467fc6c0 Level-0 table #64: 0 bytes OK 2026/05/27-14:33:21.780129 7f3e6effd6c0 Level-0 table #73: 0 bytes OK
2026/05/18-20:29:47.513818 7f5a467fc6c0 Delete type=0 #62 2026/05/27-14:33:21.786659 7f3e6effd6c0 Delete type=0 #71
2026/05/18-20:29:47.514115 7f5a467fc6c0 Manual compaction at level-0 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.800546 7f3e6effd6c0 Manual compaction at level-0 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
2026/05/18-20:29:47.514158 7f5a467fc6c0 Manual compaction at level-1 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end) 2026/05/27-14:33:21.808253 7f3e6effd6c0 Manual compaction at level-1 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+187 -2
View File
@@ -1,6 +1,20 @@
import { formatCredits } from './tradeHelper.js'; import { formatCredits } from './tradeHelper.js';
import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc } from './npcHelper.js'; import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc } from './npcHelper.js';
import { NPC_RELATIONS } from './data/npcTables.js'; import { NPC_RELATIONS } from './data/npcTables.js';
import { generateAndCreateTravellerNpc } from './travellerNpcGenerator.js';
import { generateRandomName } from './data/travellerNpcGenerator.js';
import { localizeSkill } from './mgt2eSkills.js';
import {
CITIZEN_CATEGORY_LIST,
EXPERIENCE_LEVEL_LIST,
ROLE_LIST,
GENDER_LIST,
DEFAULT_OPTIONS,
CITIZEN_CATEGORY_LABELS_FR,
EXPERIENCE_LEVEL_LABELS_FR,
ROLE_LABELS_FR,
GENDER_LABELS_FR
} from './data/travellerNpcGenerator.js';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
const MODULE_ID = 'mgt2-compendium-amiral-denisov'; const MODULE_ID = 'mgt2-compendium-amiral-denisov';
@@ -41,6 +55,19 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
context: options.context ?? 'starport', context: options.context ?? 'starport',
includeFollowUp: true, includeFollowUp: true,
}, },
mission: {},
traveller: {
citizenCategory: DEFAULT_OPTIONS.citizenCategory,
experience: DEFAULT_OPTIONS.experience,
role: DEFAULT_OPTIONS.role,
gender: DEFAULT_OPTIONS.gender,
firstName: '',
surname: '',
useRandomName: true,
createActor: DEFAULT_OPTIONS.createActor,
actorName: '',
openCreatedActor: DEFAULT_OPTIONS.openCreatedActor,
},
}; };
} }
@@ -50,6 +77,25 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
...this._formData, ...this._formData,
activeTab: this._activeTab, activeTab: this._activeTab,
relations: Object.entries(NPC_RELATIONS).map(([key, value]) => ({ key, label: value.label })), relations: Object.entries(NPC_RELATIONS).map(([key, value]) => ({ key, label: value.label })),
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
key: c.key,
label: CITIZEN_CATEGORY_LABELS_FR[c.key] || c.label,
description: c.description
})),
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
key: e.key,
label: EXPERIENCE_LEVEL_LABELS_FR[e.key] || e.label,
description: e.description
})),
roles: ROLE_LIST.map(r => ({
key: r.key,
label: ROLE_LABELS_FR[r.key] || r.label,
description: r.description
})),
genders: GENDER_LIST.map(g => ({
key: g.key,
label: GENDER_LABELS_FR[g.key] || g.label
})),
}; };
} }
@@ -84,6 +130,27 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._readForm(html); this._readForm(html);
this._activateTab($(event.currentTarget).data('tab')); this._activateTab($(event.currentTarget).data('tab'));
}); });
// Gestion des événements pour l'onglet PNJ Détaillé (Traveller)
html.find('[data-action="generate-traveller-npc"]').on('click', async (event) => {
event.preventDefault();
this._readForm(html);
await this._handleTravellerNpc();
});
html.find('[data-action="randomize-name"]').on('click', (event) => {
event.preventDefault();
this._randomizeTravellerName(html);
});
html.find('[name="traveller.useRandomName"]').on('change', (event) => {
const useRandom = event.target.checked;
this._formData.traveller.useRandomName = useRandom;
html.find('.traveller-name-fields').toggleClass('hidden', useRandom);
});
// Initialiser l'affichage des champs de nom pour l'onglet Traveller
html.find('.traveller-name-fields').toggleClass('hidden', this._formData.traveller.useRandomName);
} }
_getForm() { _getForm() {
@@ -137,6 +204,18 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._formData.npc.openCreatedActor = html.find('[name="npc.openCreatedActor"]').is(':checked'); this._formData.npc.openCreatedActor = html.find('[name="npc.openCreatedActor"]').is(':checked');
this._formData.encounter.context = html.find('[name="encounter.context"]').val(); this._formData.encounter.context = html.find('[name="encounter.context"]').val();
this._formData.encounter.includeFollowUp = html.find('[name="encounter.includeFollowUp"]').is(':checked'); this._formData.encounter.includeFollowUp = html.find('[name="encounter.includeFollowUp"]').is(':checked');
// Données pour l'onglet PNJ Détaillé (Traveller)
this._formData.traveller.citizenCategory = html.find('[name="traveller.citizenCategory"]').val();
this._formData.traveller.experience = html.find('[name="traveller.experience"]').val();
this._formData.traveller.role = html.find('[name="traveller.role"]').val();
this._formData.traveller.gender = html.find('[name="traveller.gender"]').val();
this._formData.traveller.firstName = html.find('[name="traveller.firstName"]').val();
this._formData.traveller.surname = html.find('[name="traveller.surname"]').val();
this._formData.traveller.useRandomName = html.find('[name="traveller.useRandomName"]').is(':checked');
this._formData.traveller.createActor = html.find('[name="traveller.createActor"]').is(':checked');
this._formData.traveller.actorName = html.find('[name="traveller.actorName"]').val();
this._formData.traveller.openCreatedActor = html.find('[name="traveller.openCreatedActor"]').is(':checked');
} }
async _handleNpc() { async _handleNpc() {
@@ -162,14 +241,75 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
await this._postToChatResult(result); await this._postToChatResult(result);
} }
async _handleTravellerNpc() {
const button = $(this.element).find('[data-action="generate-traveller-npc"]');
const originalLabel = button.html();
try {
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Génération...');
const generateOptions = {
citizenCategory: this._formData.traveller.citizenCategory,
experience: this._formData.traveller.experience,
role: this._formData.traveller.role,
gender: this._formData.traveller.gender,
createActor: this._formData.traveller.createActor,
actorName: this._formData.traveller.actorName,
openCreatedActor: this._formData.traveller.openCreatedActor
};
if (!this._formData.traveller.useRandomName && this._formData.traveller.firstName && this._formData.traveller.surname) {
generateOptions.firstName = this._formData.traveller.firstName;
generateOptions.surname = this._formData.traveller.surname;
}
const result = await generateAndCreateTravellerNpc(generateOptions);
if (result.success) {
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 {
button.prop('disabled', false).html(originalLabel);
}
}
_randomizeTravellerName(html) {
const name = generateRandomName(this._formData.traveller.gender);
html.find('[name="traveller.firstName"]').val(name.firstName);
html.find('[name="traveller.surname"]').val(name.surname);
this._formData.traveller.firstName = name.firstName;
this._formData.traveller.surname = name.surname;
this._formData.traveller.useRandomName = false;
html.find('[name="traveller.useRandomName"]').prop('checked', false);
html.find('.traveller-name-fields').removeClass('hidden');
}
async _postToChatResult(data) { async _postToChatResult(data) {
registerHandlebarsHelpers(); registerHandlebarsHelpers();
const html = await foundry.applications.handlebars.renderTemplate(`modules/${MODULE_ID}/templates/npc-result.hbs`, data);
// Déterminer quel template utiliser en fonction du type de données
let template = `modules/${MODULE_ID}/templates/npc-result.hbs`;
let resultType = 'npc-result';
if (data.type === 'traveller-npc' || data?.flags?.[MODULE_ID]?.type === 'traveller-npc-result') {
template = `modules/${MODULE_ID}/templates/traveller-npc-result.hbs`;
resultType = 'traveller-npc-result';
}
const html = await foundry.applications.handlebars.renderTemplate(template, data);
await ChatMessage.create({ await ChatMessage.create({
content: html, content: html,
speaker: ChatMessage.getSpeaker(), speaker: ChatMessage.getSpeaker(),
flags: { [MODULE_ID]: { type: 'npc-result' } }, flags: { [MODULE_ID]: { type: resultType } },
}); });
} }
} }
@@ -180,8 +320,53 @@ function registerHandlebarsHelpers() {
if (helpersRegistered) return; if (helpersRegistered) return;
helpersRegistered = true; helpersRegistered = true;
// Helpers existants pour NPC
Handlebars.registerHelper('eq', (a, b) => a === b); Handlebars.registerHelper('eq', (a, b) => a === b);
Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : '')); Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : ''));
Handlebars.registerHelper('formatCredits', (amount) => formatCredits(amount)); Handlebars.registerHelper('formatCredits', (amount) => formatCredits(amount));
Handlebars.registerHelper('contains', (text, search) => String(text ?? '').includes(search)); Handlebars.registerHelper('contains', (text, search) => String(text ?? '').includes(search));
// Helper pour localiser une compétence (ex: 'pilot' -> 'Pilote')
Handlebars.registerHelper('localizeSkill', (skillFqn) => {
if (!skillFqn) return '';
return localizeSkill(String(skillFqn));
});
// Helper pour joindre un tableau de compétences en les localisant
Handlebars.registerHelper('joinLocalizedSkills', (arr, sep = ', ') => {
if (!Array.isArray(arr)) return '';
return arr.map(skill => localizeSkill(String(skill))).join(sep);
});
// Helpers pour Traveller NPC
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 (Difficulté Modificateur)
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 lookup dans un objet
Handlebars.registerHelper('lookup', (obj, key) => {
if (!obj || !key) return '';
return obj[key] !== undefined ? obj[key] : '';
});
} }
+281
View File
@@ -0,0 +1,281 @@
/**
* 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,
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;
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,
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() {
registerHandlebarsHelpers();
return {
...this._formData,
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
key: c.key,
label: CITIZEN_CATEGORY_LABELS_FR[c.key] || c.label,
description: c.description
})),
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
key: e.key,
label: EXPERIENCE_LEVEL_LABELS_FR[e.key] || e.label,
description: e.description
})),
roles: ROLE_LIST.map(r => ({
key: r.key,
label: ROLE_LABELS_FR[r.key] || r.label,
description: r.description
})),
genders: GENDER_LIST.map(g => ({
key: g.key,
label: GENDER_LABELS_FR[g.key] || 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;
this._formData.useRandomName = useRandom;
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) {
// 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) {
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) {
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() {
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
// ============================================================================
// Import des données pour les helpers
import { CHARACTERISTIC_LIST, UPP_ORDER } from './data/travellerNpcGenerator.js';
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 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 });
}
+880
View File
@@ -0,0 +1,880 @@
/**
* 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',
'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'];
// ============================================================================
// 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
// ============================================================================
/**
* 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} - Objet rôle
*/
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;
}
/**
* 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
// ============================================================================
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 = {
'Pilot-Spacecraft': 'Pilote Vaisseau spatial',
'Pilot-Small Craft': 'Pilote Petits vaisseaux',
'Pilot': 'Pilote',
'Flyer': 'Pilote Aéronef atmosphérique',
'Astrogation': 'Astrogation',
'Navigation': 'Navigation',
'Electronics-Sensors': 'Électronique Capteurs',
'Electronics-Communications': 'Électronique Communications',
'Electronics-Computers': 'Électronique Informatique',
'Electronics': 'Électronique',
'Computers': 'Informatique',
'Gunner-Turrets': 'Artilleur Tourelles',
'Gunner-Screens': 'Artilleur Boucliers',
'Gunner': 'Artilleur',
'Gun Combat': 'Combat aux armes à feu',
'Heavy Weapons': 'Armes lourdes',
'Explosives': 'Explosifs',
'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',
'Steward': 'Intendant',
'Carouse': 'Festoyer',
'Persuade': 'Persuasion',
'Broker': 'Courtage',
'Admin': 'Administration',
'Advocate': 'Plaidoyer',
'Diplomat': 'Diplomatie',
'Streetwise': 'Rues',
'Leadership': 'Leadership',
'Science-Biology': 'Science Biologie',
'Science-Chemistry': 'Science Chimie',
'Science': 'Science',
'Medic': 'Médecine',
'Deception': 'Tromperie',
'Investigate': 'Investigation',
'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',
'Tactics': 'Tactiques',
'Recon': 'Reconnaissance',
'Survival': 'Survie',
'Stealth': 'Discrétion',
'Communications': 'Communications',
'Drive-Grav': 'Conduite Gravité',
'Drive': 'Conduite',
'Vacc Suit': 'Combinaison spatiale',
'Language': 'Langue',
'Art-Acting': 'Art Jeu d\'acteur',
'Art-Instrument': 'Art Instrument',
'Art': 'Art'
};
export const CHARACTERISTIC_LABELS_FR = {
'STR': 'Force',
'DEX': 'Dextérité',
'END': 'Endurance',
'INT': 'Intellect',
'EDU': 'Éducation',
'SOC': 'Statut Social'
};
export const CITIZEN_CATEGORY_LABELS_FR = {
'belowAverage': 'En dessous de la moyenne',
'average': 'Moyenne',
'aboveAverage': 'Au-dessus de la moyenne',
'exceptional': 'Exceptionnel'
};
export const EXPERIENCE_LEVEL_LABELS_FR = {
'recruit': 'Recrue',
'rookie': 'Débutant',
'intermediate': 'Intermédiaire',
'regular': 'Régulier',
'veteran': 'Vétéran',
'elite': 'Élite'
};
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'
};
export const GENDER_LABELS_FR = {
'unspecified': 'Non spécifié',
'female': 'Féminin',
'male': 'Masculin'
};
+26 -5
View File
@@ -112,13 +112,34 @@ export function setSkillLevel(skills, skillFqn, level) {
if (!skillId || !skills?.[skillId]) return skills; if (!skillId || !skills?.[skillId]) return skills;
const numericLevel = Number(level ?? 0); const numericLevel = Number(level ?? 0);
const skill = foundry.utils.mergeObject(skills[skillId], { trained: numericLevel > 0 || skills[skillId].trained }); let skill = foundry.utils.deepClone(skills[skillId]);
if (specialityId && skill.specialities?.[specialityId]) { // Marquer la compétence comme entraînée si niveau > 0
skill.specialities[specialityId] = foundry.utils.mergeObject(skill.specialities[specialityId], { skill.trained = numericLevel > 0 || skill.trained;
value: Math.max(Number(skill.specialities[specialityId].value ?? 0), numericLevel),
}); if (specialityId) {
// Créer l'objet specialities s'il n'existe pas
if (!skill.specialities) {
skill.specialities = {};
}
// Créer la spécialité si elle n'existe pas
if (!skill.specialities[specialityId]) {
skill.specialities[specialityId] = {
value: 0,
trained: false,
default: skill.default || null
};
}
// Appliquer le niveau à la spécialité
skill.specialities[specialityId] = foundry.utils.mergeObject(
skill.specialities[specialityId],
{
value: Math.max(Number(skill.specialities[specialityId].value ?? 0), numericLevel),
trained: true
}
);
} else { } else {
// Compétence sans spécialité
skill.value = Math.max(Number(skill.value ?? 0), numericLevel); skill.value = Math.max(Number(skill.value ?? 0), numericLevel);
} }
+3 -1
View File
@@ -35,6 +35,8 @@ Hooks.once('init', () => {
loadTemplatesFn([ loadTemplatesFn([
`modules/${MODULE_ID}/templates/npc-dialog.hbs`, `modules/${MODULE_ID}/templates/npc-dialog.hbs`,
`modules/${MODULE_ID}/templates/npc-result.hbs`, `modules/${MODULE_ID}/templates/npc-result.hbs`,
`modules/${MODULE_ID}/templates/traveller-npc-dialog.hbs`,
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
]); ]);
} }
@@ -97,7 +99,7 @@ Hooks.on('renderChatInput', (app, html, data) => {
}); });
/** /**
* 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é) * Utilise preCreateChatMessage pour Foundry v14+ (avant que le message ne soit validé)
* Compatible avec Foundry v13 et v14 * Compatible avec Foundry v13 et v14
*/ */
File diff suppressed because it is too large Load Diff
+730
View File
@@ -0,0 +1,730 @@
/**
* Traveller NPC Generator - Logique métier
* Basé sur : https://github.com/carloscasalar/traveller-npc-generator
*
* Ce fichier contient la logique de génération des PNJ Traveller.
*/
import {
CITIZEN_CATEGORY,
EXPERIENCE_LEVEL,
ROLE,
ROLE_SKILLS,
CHARACTERISTIC_PRIORITIES,
GENDER,
CHARACTERISTIC,
CHARACTERISTIC_LIST,
UPP_ORDER,
toHex,
calculateDm,
pickRandomItem,
shuffleArray,
getRoleByKey,
getCitizenCategoryByKey,
getExperienceLevelByKey,
getGenderByKey,
getSkillsForRole,
getCharacteristicPrioritiesForRole,
validateOptions,
DEFAULT_OPTIONS,
NAME_CATALOGS,
SKILL_LABELS_FR,
CHARACTERISTIC_LABELS_FR,
CITIZEN_CATEGORY_LABELS_FR,
EXPERIENCE_LEVEL_LABELS_FR,
ROLE_LABELS_FR,
GENDER_LABELS_FR
} from './data/travellerNpcGenerator.js';
import { setSkillLevel, localizeSkill } from './mgt2eSkills.js';
import { travellerNpcCache, TravellerNpcError, ERROR_CODES } from './utils/travellerNpcUtils.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
// ============================================================================
// Conversion des compétences
// ============================================================================
/**
* Mapping des compétences Traveller vers mgt2e
* @type {Object<string, string>}
*/
// Mapping des compétences Traveller vers MgT2e
// IMPORTANT: Utiliser les clés internes EXACTES du système mgt2e (ex: pilot, spacecraft, smallCraft)
// Basé sur le fichier fr.json du système : /home/morr/work/foundryvtt/traveller-foundryvtt/mgt2e/lang/fr.json
// Format: 'Compétence-Traveller' -> 'competence_mgt2e.specialite_mgt2e' (en camelCase, sans accents)
// Les clés sont en anglais, les libellés français sont gérés par la localisation
const SKILL_MAPPING = {
// Pilotage
'Pilot-Spacecraft': 'pilot.spacecraft',
'Pilot-Small Craft': 'pilot.smallCraft',
'Pilot': 'pilot',
'Flyer': 'flyer',
// Astrogation et Navigation
'Astrogation': 'astrogation',
'Navigation': 'navigation',
// Électronique
'Electronics-Sensors': 'electronics.sensors',
'Electronics-Communications': 'electronics.comms',
'Electronics-Computers': 'electronics.computers',
'Electronics': 'electronics',
'Computers': 'electronics',
'Communications': 'electronics',
// Artillerie
'Gunner-Turrets': 'gunner.turret',
'Gunner-Screens': 'gunner.screen',
'Gunner': 'gunner',
// Mécanique
'Mechanic': 'mechanic',
// Ingénierie
'Engineer-MDrive': 'engineer.mDrive',
'Engineer-Power': 'engineer.power',
'Engineer-JDrive': 'engineer.jDrive',
'Engineer-Life Support': 'engineer.lifeSupport',
'Engineer': 'engineer',
// Social et Administration
'Steward': 'steward',
'Carouse': 'carouse',
'Persuade': 'persuade',
'Broker': 'broker',
'Admin': 'admin',
'Language': 'language',
'Advocate': 'advocate',
'Leadership': 'leadership',
'Medic': 'medic',
'Diplomat': 'diplomat',
// Sciences
'Science-Biology': 'science.biology',
'Science-Chemistry': 'science.chemistry',
'Science': 'science',
// Combat
'Gun Combat': 'guncombat',
'Heavy Weapons': 'heavyweapons',
// Mêlée
'Melee-Unarmed': 'melee.unarmed',
'Melee-Blade': 'melee.blade',
'Melee': 'melee',
// Athlétisme
'Athletics-Strength': 'athletics.strength',
'Athletics-Dexterity': 'athletics.dexterity',
'Athletics': 'athletics',
// Tactique et Exploration
'Tactics': 'tactics',
'Recon': 'recon',
'Survival': 'survival',
'Stealth': 'stealth',
'Explosives': 'explosives',
'Deception': 'deception',
'Investigate': 'investigate',
// Conduite
'Drive-Grav': 'drive.grav',
'Drive': 'drive',
// Équipement
'Vacc Suit': 'vaccsuit',
// Art
'Art-Acting': 'art.performer',
'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) {
// Vérifier d'abord dans le mapping explicite
if (SKILL_MAPPING[skillName]) {
return SKILL_MAPPING[skillName];
}
// Si pas dans le mapping, essayer de deviner
// MgT2e utilise des noms en minuscules (ex: pilot, electronics, guncombat)
// 1. Remplacer les tirets et espaces par des points
// 2. Tout mettre en minuscules
return skillName.toLowerCase().replace(/-| /g, '.');
}
/**
* Obtient le libellé français d'une compétence Traveller
* @param {string} skillName - Nom de la compétence (ex: 'Pilot-Spacecraft')
* @returns {string} - Libellé français
*/
function getSkillLabelFr(skillName) {
return SKILL_LABELS_FR[skillName] || skillName;
}
/**
* Obtient le libellé français d'une caractéristique
* @param {string} charKey - Clé de la caractéristique (ex: 'STR')
* @returns {string} - Libellé français
*/
function getCharacteristicLabelFr(charKey) {
return CHARACTERISTIC_LABELS_FR[charKey] || charKey;
}
// ============================================================================
// Génération des caractéristiques
// ============================================================================
/**
* Génère les caractéristiques d'un PNJ en fonction de sa catégorie et de son rôle
*
* @param {string} citizenCategoryKey - Clé de la catégorie de citoyen
* @param {string} roleKey - Clé du rôle
* @returns {Object} - Objet contenant les caractéristiques et l'UPP
*/
export function generateCharacteristics(citizenCategoryKey, roleKey) {
const category = getCitizenCategoryByKey(citizenCategoryKey);
const priorities = getCharacteristicPrioritiesForRole(roleKey);
// Cloner et mélanger l'array de base de la catégorie
let characteristicArray = shuffleArray([...category.characteristicArray]);
const characteristics = {};
// Créer un tableau de priorités avec leur ordre
const priorityGroups = [
{ keys: priorities.high, count: priorities.high.length },
{ keys: priorities.medium, count: priorities.medium.length },
{ keys: priorities.low, count: priorities.low.length }
];
// Attribuer les valeurs dans l'ordre de priorité
for (const group of priorityGroups) {
const values = characteristicArray.slice(0, group.count);
characteristicArray = characteristicArray.slice(group.count);
group.keys.forEach((charKey, i) => {
characteristics[charKey] = values[i] ?? 7;
});
}
// Remplir les valeurs manquantes avec 7
for (const charKey of UPP_ORDER) {
if (characteristics[charKey] === undefined) {
characteristics[charKey] = 7;
}
}
// Construire l'UPP
const upp = UPP_ORDER.map(charKey => toHex(characteristics[charKey])).join('');
return {
characteristics,
upp,
category
};
}
// ============================================================================
// Génération des compétences
// ============================================================================
/**
* Distribue les niveaux de compétence sur une liste de compétences
*
* @param {string[]} roleSkills - Liste des compétences du rôle
* @param {Object} distribution - Distribution des niveaux
* @returns {Array<{name: string, level: number}>} - Compétences avec niveaux
*/
function distributeSkillLevels(roleSkills, distribution) {
// Supprimer les doublons EXACTS (même nom complet de compétence)
// mais conserver les spécialisations comme Pilot-Spacecraft et Pilot-Small Craft
const uniqueSkills = [];
const seen = new Set();
for (const skill of roleSkills) {
if (!seen.has(skill)) {
seen.add(skill);
uniqueSkills.push(skill);
}
}
// Mélanger les compétences une seule fois
const shuffledSkills = shuffleArray([...uniqueSkills]);
// Créer un tableau de niveaux, initialisé à 0
const levels = new Array(shuffledSkills.length).fill(0);
// Distribuer les niveaux de manière séquentielle
let index = 0;
// Level 3 (les plus rares, en premier)
const level3Count = Math.min(distribution.level3, shuffledSkills.length);
for (let i = 0; i < level3Count; i++) {
if (index < levels.length) levels[index++] = 3;
}
// Level 2
const level2Count = Math.min(distribution.level2, shuffledSkills.length - level3Count);
for (let i = 0; i < level2Count; i++) {
if (index < levels.length) levels[index++] = 2;
}
// Level 1
const level1Count = Math.min(distribution.level1, shuffledSkills.length - level3Count - level2Count);
for (let i = 0; i < level1Count; i++) {
if (index < levels.length) levels[index++] = 1;
}
// Level 0 (déjà initialisé)
// Créer le résultat trié par niveau (descendant) puis par nom
return shuffledSkills
.map((skill, i) => ({ name: skill, level: levels[i] }))
.sort((a, b) => b.level - a.level || a.name.localeCompare(b.name));
}
/**
* Génère les compétences d'un PNJ en fonction de son rôle et de son expérience
*
* @param {string} roleKey - Clé du rôle
* @param {string} experienceKey - Clé du niveau d'expérience
* @returns {Array<{name: string, level: number}>} - Liste des compétences avec niveaux
*/
export function generateSkills(roleKey, experienceKey) {
const roleSkills = getSkillsForRole(roleKey);
const experience = getExperienceLevelByKey(experienceKey);
const distribution = experience.skillDistribution;
return distributeSkillLevels(roleSkills, distribution);
}
// ============================================================================
// Génération complète du PNJ
// ============================================================================
/**
* Génère un PNJ Traveller complet
*
* @typedef {Object} TravellerNpcOptions
* @property {string} [citizenCategory] - Catégorie de citoyen
* @property {string} [experience] - Niveau d'expérience
* @property {string} [role] - Rôle
* @property {string} [gender] - Genre
* @property {string} [firstName] - Prénom forcé
* @property {string} [surname] - Nom de famille forcé
*
* @typedef {Object} TravellerNpcResult
* @property {boolean} success - Succès de la génération
* @property {string} type - Type de résultat
* @property {Object} name - Nom du PNJ
* @property {Object} role - Rôle
* @property {Object} citizenCategory - Catégorie
* @property {Object} experience - Expérience
* @property {Object} gender - Genre
* @property {Object} characteristics - Caractéristiques
* @property {string} upp - Code UPP
* @property {Array<{name: string, level: number}>} skills - Compétences
* @property {Array<{name: string, level: number}>} skillsForActor - Compétences pour mgt2e
* @property {Object} display - Métadonnées pour l'affichage
*
* @param {TravellerNpcOptions} [options={}]
* @returns {TravellerNpcResult}
*/
export function generateTravellerNpc(options = {}) {
// Valider et fusionner avec les options par défaut
const opts = validateOptions({
...DEFAULT_OPTIONS,
...options
});
// Générer le nom
let name;
if (opts.firstName && opts.surname) {
name = {
firstName: opts.firstName,
surname: opts.surname,
fullName: `${opts.firstName} ${opts.surname}`
};
} else {
const firstName = pickRandomItem(
opts.gender === 'female' ? NAME_CATALOGS.femaleNames :
opts.gender === 'male' ? NAME_CATALOGS.maleNames :
NAME_CATALOGS.nonGenderedNames
);
const surname = pickRandomItem(NAME_CATALOGS.surnames);
name = {
firstName,
surname,
fullName: `${firstName} ${surname}`
};
}
// Générer les caractéristiques
const { characteristics, upp, category } = generateCharacteristics(
opts.citizenCategory,
opts.role
);
// Générer les compétences
const skills = generateSkills(opts.role, opts.experience);
// Convertir les compétences au format mgt2e pour la création de fiche
const skillsForActor = skills.map(s => ({
name: convertSkillToMgt2eFormat(s.name),
level: s.level
}));
// Récupérer les objets complets pour les références
const citizenCategory = getCitizenCategoryByKey(opts.citizenCategory);
const experience = getExperienceLevelByKey(opts.experience);
const role = getRoleByKey(opts.role);
const gender = getGenderByKey(opts.gender);
// Libellés des caractéristiques pour l'affichage (en français)
const characteristicLabels = {};
CHARACTERISTIC_LIST.forEach(char => {
characteristicLabels[char.key] = getCharacteristicLabelFr(char.key);
});
// Ajouter les libellés français aux compétences pour l'affichage
const skillsWithLabels = skills.map(skill => ({
...skill,
labelFr: getSkillLabelFr(skill.name)
}));
return {
success: true,
type: 'traveller-npc',
name,
role,
citizenCategory,
experience,
gender,
characteristics,
upp,
skills: skillsWithLabels,
skillsForActor,
MODULE_ID,
UPP_ORDER,
display: {
roleLabel: ROLE_LABELS_FR[role.key] || role.label,
categoryLabel: CITIZEN_CATEGORY_LABELS_FR[citizenCategory.key] || citizenCategory.label,
experienceLabel: EXPERIENCE_LEVEL_LABELS_FR[experience.key] || experience.label,
genderLabel: GENDER_LABELS_FR[gender.key] || gender.label,
characteristicLabels
}
};
}
// ============================================================================
// Récupération du système de base mgt2e
// ============================================================================
/**
* Récupère le système de base des acteurs mgt2e
* @returns {Promise<Object|null>} - Système de base ou null
*/
async function fetchMgt2eBaseActorSystem() {
try {
// Vérifier que le système mgt2e est actif
if (game.system?.id !== 'mgt2e') {
console.warn(`${MODULE_ID} | Le système mgt2e n'est pas actif`);
return null;
}
const pack = game.packs.get('mgt2e.base-actors');
if (!pack) {
console.warn(`${MODULE_ID} | Le compendium mgt2e.base-actors n'est pas disponible`);
return null;
}
const index = Array.from(await pack.getIndex({ fields: ['name', 'type'] }));
const entry = index.find((document) => document.name === 'DEFAULT TRAVELLER')
?? index.find((document) => document.type === 'traveller')
?? index[0];
if (!entry?._id) {
console.warn(`${MODULE_ID} | Aucun acteur de base trouvé dans mgt2e.base-actors`);
return null;
}
const document = await pack.getDocument(entry._id);
const system = document?.toObject()?.system;
if (!system) {
console.warn(`${MODULE_ID} | Le système de l'acteur de base est vide`);
return null;
}
return system;
} catch (error) {
console.error(`${MODULE_ID} | Erreur lors de la récupération du système de base mgt2e:`, error);
throw new TravellerNpcError(
'Erreur lors de la récupération du système de base mgt2e',
ERROR_CODES.BASE_ACTOR_NOT_FOUND,
{ error: error.message }
);
}
}
/**
* Récupère le système de base des acteurs mgt2e avec cache
* @param {Object} [options={}] - Options
* @param {boolean} [options.forceRefresh=false] - Forcer le rafraîchissement
* @returns {Promise<Object|null>}
*/
export async function getMgt2eBaseActorSystem(options = {}) {
const { forceRefresh = false } = options;
const cacheKey = 'baseActorSystem';
return travellerNpcCache.getOrFetch(
cacheKey,
fetchMgt2eBaseActorSystem,
{ forceRefresh }
);
}
// ============================================================================
// Construction des données pour l'acteur
// ============================================================================
/**
* Construit les caractéristiques au format mgt2e
*
* @param {Object} existingCharacteristics - Caractéristiques existantes
* @param {Object} characteristics - Caractéristiques générées
* @returns {Object} - Caractéristiques au format mgt2e
*/
export function buildMgt2eCharacteristics(existingCharacteristics = {}, characteristics) {
const result = foundry.utils.deepClone(existingCharacteristics);
for (const char of CHARACTERISTIC_LIST) {
const value = characteristics[char.key] || 7;
result[char.mgt2eKey] = foundry.utils.mergeObject(result[char.mgt2eKey] ?? {}, {
value,
current: value,
dm: calculateDm(value),
show: true,
default: false,
});
}
return result;
}
/**
* Construit les compétences au format mgt2e
*
* @param {Object} existingSkills - Compétences existantes
* @param {Array<{name: string, level: number}>} skills - Compétences générées
* @param {boolean} [alreadyMapped=false] - Les compétences sont déjà au format mgt2e
* @returns {Object} - Compétences au format mgt2e
*/
export function buildMgt2eSkills(existingSkills = {}, skills, alreadyMapped = false) {
const result = foundry.utils.deepClone(existingSkills);
for (const { name, level } of skills) {
const skillName = alreadyMapped ? name : convertSkillToMgt2eFormat(name);
setSkillLevel(result, skillName, level);
}
return result;
}
/**
* Construit la description de l'acteur
*
* @param {Object} npcData - Données du PNJ généré
* @param {string} actorName - Nom de l'acteur
* @returns {string} - Description formatée
*/
function buildActorDescription(npcData, actorName) {
const notableSkills = npcData.skills
.filter(s => s.level > 0)
.map(s => {
try {
return localizeSkill(s.name) || s.name;
} catch (e) {
return s.name;
}
})
.join(', ');
const lines = [
`${actorName}${npcData.role.label}`,
`Catégorie : ${npcData.citizenCategory.label}`,
`Expérience : ${npcData.experience.label}`,
`UPP : ${npcData.upp}`,
`Genre : ${npcData.gender.label}`
];
if (notableSkills) {
lines.push(`Compétences : ${notableSkills}`);
}
return lines.join('\n');
}
// ============================================================================
// Création de la fiche d'acteur
// ============================================================================
/**
* Crée une fiche d'acteur pour un PNJ Traveller
*
* @param {Object} npcData - Données du PNJ généré
* @param {Object} options - Options de création
* @param {string} [options.name] - Nom de l'acteur
* @param {boolean} [options.openSheet=true] - Ouvrir la fiche après création
* @returns {Promise<Actor|null>} - Acteur créé ou null
*/
export async function createTravellerNpcActor(npcData, options = {}) {
try {
const requestedName = options.name?.trim();
const actorName = requestedName || npcData.name.fullName || `PNJ — ${npcData.role.label}`;
// Vérifier que mgt2e est actif
if (game.system?.id !== 'mgt2e') {
throw new TravellerNpcError(
'Le système mgt2e doit être actif pour créer des fiches PNJ Traveller',
ERROR_CODES.MGT2E_NOT_ACTIVE
);
}
// Récupérer le système de base
const baseActorSystem = await getMgt2eBaseActorSystem();
// Construire la description (avec <br> pour HTML)
const description = buildActorDescription(npcData, actorName).replace(/\n/g, '<br>');
const actorData = {
name: actorName,
type: 'npc',
img: 'systems/mgt2e/icons/cargo/passenger-middle.svg',
system: {
settings: foundry.utils.mergeObject(
foundry.utils.deepClone(baseActorSystem?.settings ?? {}),
{
hideUntrained: true,
lockCharacteristics: true,
}
),
sophont: foundry.utils.mergeObject(
foundry.utils.deepClone(baseActorSystem?.sophont ?? {}),
{
age: 18 + Math.floor(Math.random() * 40),
homeworld: '',
profession: npcData.role.label,
}
),
characteristics: buildMgt2eCharacteristics(
foundry.utils.deepClone(baseActorSystem?.characteristics ?? {}),
npcData.characteristics
),
hits: foundry.utils.deepClone(baseActorSystem?.hits ?? {}),
skills: buildMgt2eSkills(
foundry.utils.deepClone(baseActorSystem?.skills ?? {}),
npcData.skillsForActor,
true
),
description,
},
flags: {
[MODULE_ID]: {
generatedTravellerNpc: {
version: 1,
role: npcData.role.key,
citizenCategory: npcData.citizenCategory.key,
experience: npcData.experience.key,
gender: npcData.gender.key,
upp: npcData.upp,
generatedAt: new Date().toISOString()
},
},
},
};
const actor = await Actor.create(actorData, { renderSheet: false });
if (options.openSheet !== false) {
actor.sheet?.render(true);
}
return actor;
} catch (error) {
const npcError = TravellerNpcError.from(error, ERROR_CODES.ACTOR_CREATION_FAILED);
npcError.notify();
return null;
}
}
// ============================================================================
// Fonction principale
// ============================================================================
/**
* Fonction principale pour générer un PNJ Traveller
* Peut créer une fiche d'acteur si demandé
*
* @param {TravellerNpcOptions} [options={}]
* @returns {Promise<TravellerNpcResult>}
*/
export async function generateAndCreateTravellerNpc(options = {}) {
const npcData = generateTravellerNpc(options);
if (options.createActor) {
const actor = await createTravellerNpcActor(npcData, {
name: options.actorName,
openSheet: options.openCreatedActor !== false
});
if (actor) {
npcData.createdActor = {
id: actor.id,
name: actor.name
};
}
}
return npcData;
}
// ============================================================================
// Export des types et données pour compatibilité
// ============================================================================
// Ré-exporter les données pour facilitier les imports
export {
CITIZEN_CATEGORY,
CITIZEN_CATEGORY_LIST,
EXPERIENCE_LEVEL,
EXPERIENCE_LEVEL_LIST,
ROLE,
ROLE_LIST,
ROLE_SKILLS,
CHARACTERISTIC_PRIORITIES,
GENDER,
GENDER_LIST,
CHARACTERISTIC,
CHARACTERISTIC_LIST,
UPP_ORDER,
toHex,
calculateDm,
pickRandomItem,
shuffleArray,
getRoleByKey,
getCitizenCategoryByKey,
getExperienceLevelByKey,
getGenderByKey,
getSkillsForRole,
getCharacteristicPrioritiesForRole,
validateOptions,
DEFAULT_OPTIONS
} from './data/travellerNpcGenerator.js';
+272
View File
@@ -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
};
+614
View File
@@ -0,0 +1,614 @@
/**
* 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 - Aligné sur mgt2-npc-dialog et mgt2-commerce-dialog
======================================================================== */
.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;
}
.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 {
margin: 0 0 10px;
color: #555;
font-size: 0.87em;
line-height: 1.5;
}
/* ==========================================================================
Champs de formulaire - Aligné sur mgt2-npc-form
======================================================================== */
.mgt2-traveller-npc-form .form-group {
display: flex;
flex-direction: column;
gap: 3px;
margin: 0;
}
.mgt2-traveller-npc-form .form-group-row {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.mgt2-traveller-npc-form .form-group-row .form-group {
flex: 1;
}
.mgt2-traveller-npc-form label {
font-size: 0.8em;
font-weight: bold;
color: #444;
display: block;
margin-bottom: 3px;
}
.mgt2-traveller-npc-form input[type="text"],
.mgt2-traveller-npc-form select {
width: 100%;
box-sizing: border-box;
padding: 5px 7px;
font-size: 0.85em;
background: #fff;
color: #222;
border: 1px solid #bbb;
border-radius: 3px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.08);
height: 28px;
}
.mgt2-traveller-npc-form input[type="text"]:focus,
.mgt2-traveller-npc-form select:focus {
border-color: #c9a227;
outline: none;
box-shadow: 0 0 0 2px rgba(201, 162, 39, 0.22);
}
.mgt2-traveller-npc-form select option {
background: #fff;
color: #222;
}
/* ==========================================================================
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;
align-items: flex-end;
}
.mgt2-traveller-npc-form .name-fields.hidden {
display: none;
}
.mgt2-traveller-npc-form .name-fields .form-group {
flex: 1;
}
.mgt2-traveller-npc-form .name-fields .btn-small {
padding: 5px 10px;
background: #2c2c3e;
color: #e1bc57;
border: 1px solid #c9a227;
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: #243852;
color: #f2d27a;
}
/* ==========================================================================
Checkbox
======================================================================== */
.mgt2-traveller-npc-form .checkbox-group {
margin: 10px 0;
}
.mgt2-traveller-npc-form .checkbox-group label {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.85em;
color: #333;
cursor: pointer;
}
.mgt2-traveller-npc-form .checkbox-group input[type="checkbox"] {
accent-color: #c9a227;
width: 14px;
height: 14px;
margin: 0;
}
/* ==========================================================================
Hint
======================================================================== */
.mgt2-traveller-npc-form .hint {
font-weight: normal;
font-size: 0.85em;
color: #777;
margin-top: 4px;
}
/* ==========================================================================
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 {
background: #2c2c3e;
color: #e1bc57;
border: 1px solid #c9a227;
border-radius: 4px;
padding: 7px 18px;
font-size: 0.85em;
font-weight: bold;
cursor: pointer;
text-shadow: none;
}
button.btn-calculate:hover,
.mgt2-traveller-npc-form .btn-calculate:hover {
background: #243852;
color: #f2d27a;
}
button.btn-calculate:disabled,
.mgt2-traveller-npc-form .btn-calculate:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mgt2-traveller-npc-form .btn-calculate i {
margin-right: 8px;
}
/* ==========================================================================
Résultat - Aligné sur mgt2-npc-result
======================================================================== */
.mgt2-npc-result,
.traveller-npc-result {
font-size: 0.85em;
color: #222;
}
.traveller-npc-result .npc-header {
text-align: center;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 2px solid #c9a227;
}
.traveller-npc-result .npc-header h3 {
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 {
margin-right: 8px;
}
.traveller-npc-result .npc-name {
font-size: 1.3em;
font-weight: bold;
color: #222;
}
.traveller-npc-result .npc-notice {
padding: 8px 12px;
margin-bottom: 15px;
border-radius: 4px;
font-size: 0.9em;
text-align: center;
}
.traveller-npc-result .npc-notice.success {
background: #eef8ee;
border: 1px solid #a9d0a9;
color: #2a6a2a;
}
.traveller-npc-result .npc-notice.success i {
margin-right: 8px;
}
.traveller-npc-result .npc-details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 8px;
margin-bottom: 15px;
}
.traveller-npc-result .npc-detail {
background: #fbf8f1;
padding: 6px 8px;
border-radius: 3px;
text-align: center;
border: 1px solid #d7ccb0;
}
.traveller-npc-result .npc-detail-label {
font-size: 0.75em;
color: #7a5c00;
margin-bottom: 3px;
font-weight: bold;
}
.traveller-npc-result .npc-detail-value {
font-weight: bold;
color: #222;
font-size: 0.9em;
}
.traveller-npc-result .npc-section {
margin-bottom: 15px;
}
.traveller-npc-result .npc-section h4 {
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 {
margin-right: 6px;
}
.traveller-npc-result .npc-characteristics {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.traveller-npc-result .npc-characteristic {
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: 0.7em;
color: #7a5c00;
text-transform: uppercase;
margin-bottom: 2px;
font-weight: bold;
}
.traveller-npc-result .npc-char-value {
font-size: 1.1em;
font-weight: bold;
color: #222;
}
.traveller-npc-result .npc-char-dm {
font-size: 0.7em;
color: #c9a227;
font-weight: bold;
}
.traveller-npc-result .npc-skills {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
}
.traveller-npc-result .npc-skill {
background: #fbf8f1;
padding: 4px 8px;
border-radius: 3px;
font-size: 0.85em;
display: inline-flex;
align-items: center;
gap: 4px;
border: 1px solid #d7ccb0;
}
.traveller-npc-result .npc-skill-name {
color: #222;
}
.traveller-npc-result .npc-skill-level {
color: #c9a227;
font-weight: bold;
}
.traveller-npc-result .npc-footer {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #d7ccb0;
text-align: center;
font-size: 0.75em;
color: #666;
}
/* ==========================================================================
Niveaux de compétence
======================================================================== */
.traveller-npc-result .skillLevelSymbol {
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;
}
}
+113
View File
@@ -10,6 +10,9 @@
<a class="item {{#if (eq activeTab "mission")}}active{{/if}}" data-tab="mission"> <a class="item {{#if (eq activeTab "mission")}}active{{/if}}" data-tab="mission">
<i class="fas fa-briefcase"></i> Client & mission <i class="fas fa-briefcase"></i> Client & mission
</a> </a>
<a class="item {{#if (eq activeTab "traveller")}}active{{/if}}" data-tab="traveller">
<i class="fas fa-user-astronaut"></i> PNJ Détaillé
</a>
</nav> </nav>
<section class="tab-content"> <section class="tab-content">
@@ -119,6 +122,116 @@
</button> </button>
</div> </div>
</div> </div>
<div class="tab {{#if (eq activeTab "traveller")}}active{{/if}}" data-tab="traveller">
<h3><i class="fas fa-user-astronaut"></i> Générateur de PNJ Traveller</h3>
<p class="traveller-npc-intro">
Génère un personnage non-joueur selon les règles du générateur Traveller,
avec caractéristiques, compétences et rôle aléatoires ou personnalisés.
</p>
<fieldset>
<legend>Identité du PNJ</legend>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="traveller.useRandomName" {{#if traveller.useRandomName}}checked{{/if}}>
Utiliser un nom aléatoire
</label>
</div>
<div class="form-group-row traveller-name-fields {{#if traveller.useRandomName}}hidden{{/if}}">
<div class="form-group">
<label for="traveller-firstName">Prénom</label>
<input id="traveller-firstName" name="traveller.firstName" type="text" value="{{traveller.firstName}}" placeholder="John">
</div>
<div class="form-group">
<label for="traveller-surname">Nom de famille</label>
<input id="traveller-surname" name="traveller.surname" type="text" value="{{traveller.surname}}" placeholder="Smith">
</div>
<div class="form-group">
<button type="button" class="btn-small" data-action="randomize-name" title="Générer un nom aléatoire">
<i class="fas fa-dice-d6"></i>
</button>
</div>
</div>
<div class="form-group-row">
<div class="form-group">
<label for="traveller-gender">Genre</label>
<select id="traveller-gender" name="traveller.gender">
{{#each genders}}
<option value="{{key}}" {{#if (eq ../traveller.gender key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label for="traveller-role">Rôle <span class="required">*</span></label>
<select id="traveller-role" name="traveller.role" required>
{{#each roles}}
<option value="{{key}}" {{#if (eq ../traveller.role key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
</div>
</div>
</fieldset>
<fieldset>
<legend>Caractéristiques et Expérience</legend>
<div class="form-group-row">
<div class="form-group">
<label for="traveller-citizenCategory">Catégorie de citoyen</label>
<select id="traveller-citizenCategory" name="traveller.citizenCategory">
{{#each citizenCategories}}
<option value="{{key}}" {{#if (eq ../traveller.citizenCategory key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
<div class="hint">{{description}}</div>
</div>
<div class="form-group">
<label for="traveller-experience">Niveau d'expérience</label>
<select id="traveller-experience" name="traveller.experience">
{{#each experienceLevels}}
<option value="{{key}}" {{#if (eq ../traveller.experience key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
<div class="hint">{{description}}</div>
</div>
</div>
</fieldset>
<fieldset>
<legend>Création de fiche d'acteur</legend>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="traveller.createActor" {{#if traveller.createActor}}checked{{/if}}>
Créer une fiche PNJ dans les Acteurs
</label>
</div>
<div class="form-group-row">
<div class="form-group">
<label for="traveller-actorName">Nom de la fiche <span class="hint">(facultatif)</span></label>
<input id="traveller-actorName" name="traveller.actorName" type="text" value="{{traveller.actorName}}" placeholder="PNJ — Pilote">
</div>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="traveller.openCreatedActor" {{#if traveller.openCreatedActor}}checked{{/if}}>
Ouvrir automatiquement la fiche créée
</label>
</div>
</fieldset>
<div class="form-footer">
<button type="button" class="btn-calculate" data-action="generate-traveller-npc">
<i class="fas fa-dice-d6"></i> Générer le PNJ Traveller
</button>
</div>
</div>
</section> </section>
</form> </form>
+1 -1
View File
@@ -34,7 +34,7 @@
<div class="npc-section"> <div class="npc-section">
<div class="npc-section-title">Résumé jouable</div> <div class="npc-section-title">Résumé jouable</div>
<p><strong>Relation :</strong> {{relation.summary}}</p> <p><strong>Relation :</strong> {{relation.summary}}</p>
<p><strong>Compétences-types :</strong> {{join experience.profile.skills ", "}}</p> <p><strong>Compétences-types :</strong> {{joinLocalizedSkills experience.profile.skills ", "}}</p>
<p><strong>Niveau moyen de compétence :</strong> {{experience.profile.skillLevel}}</p> <p><strong>Niveau moyen de compétence :</strong> {{experience.profile.skillLevel}}</p>
<p><strong>Bonus de caractéristiques :</strong> <p><strong>Bonus de caractéristiques :</strong>
{{#if experience.profile.characteristicBonuses.length}} {{#if experience.profile.characteristicBonuses.length}}
+109
View File
@@ -0,0 +1,109 @@
<form class="mgt2-traveller-npc-form">
<h3><i class="fas fa-user-astronaut"></i> Générateur de PNJ Traveller</h3>
<p class="traveller-npc-intro">
Génère un personnage non-joueur selon les règles du générateur Traveller,
avec caractéristiques, compétences et rôle aléatoires ou personnalisés.
</p>
<fieldset>
<legend>Identité du PNJ</legend>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="useRandomName" {{#if useRandomName}}checked{{/if}}>
Utiliser un nom aléatoire
</label>
</div>
<div class="form-group-row name-fields {{#if useRandomName}}hidden{{/if}}">
<div class="form-group">
<label for="firstName">Prénom</label>
<input id="firstName" name="firstName" type="text" value="{{firstName}}" placeholder="John">
</div>
<div class="form-group">
<label for="surname">Nom de famille</label>
<input id="surname" name="surname" type="text" value="{{surname}}" placeholder="Smith">
</div>
<div class="form-group">
<button type="button" class="btn-small" data-action="randomize-name" title="Générer un nom aléatoire">
<i class="fas fa-dice-d6"></i>
</button>
</div>
</div>
<div class="form-group-row">
<div class="form-group">
<label for="gender">Genre</label>
<select id="gender" name="gender">
{{#each genders}}
<option value="{{key}}" {{#if (eq ../gender key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label for="role">Rôle <span class="required">*</span></label>
<select id="role" name="role" required>
{{#each roles}}
<option value="{{key}}" {{#if (eq ../role key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
</div>
</div>
</fieldset>
<fieldset>
<legend>Caractéristiques et Expérience</legend>
<div class="form-group-row">
<div class="form-group">
<label for="citizenCategory">Catégorie de citoyen</label>
<select id="citizenCategory" name="citizenCategory">
{{#each citizenCategories}}
<option value="{{key}}" {{#if (eq ../citizenCategory key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
<div class="hint">{{description}}</div>
</div>
<div class="form-group">
<label for="experience">Niveau d'expérience</label>
<select id="experience" name="experience">
{{#each experienceLevels}}
<option value="{{key}}" {{#if (eq ../experience key)}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
<div class="hint">{{description}}</div>
</div>
</div>
</fieldset>
<fieldset>
<legend>Création de fiche d'acteur</legend>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="createActor" {{#if createActor}}checked{{/if}}>
Créer une fiche PNJ dans les Acteurs
</label>
</div>
<div class="form-group-row">
<div class="form-group">
<label for="actorName">Nom de la fiche <span class="hint">(facultatif)</span></label>
<input id="actorName" name="actorName" type="text" value="{{actorName}}" placeholder="PNJ — Pilote">
</div>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" name="openCreatedActor" {{#if openCreatedActor}}checked{{/if}}>
Ouvrir automatiquement la fiche créée
</label>
</div>
</fieldset>
<div class="form-footer">
<button type="button" class="btn-calculate" data-action="generate-traveller-npc">
<i class="fas fa-dice-d6"></i> Générer le PNJ Traveller
</button>
</div>
</form>
+65
View File
@@ -0,0 +1,65 @@
<div class="mgt2-npc-result traveller-npc-result">
<div class="npc-header">
<h3><i class="fas fa-user-astronaut"></i> PNJ Traveller généré</h3>
<div class="npc-name">{{name.fullName}}</div>
</div>
{{#if createdActor}}
<div class="npc-notice success">
<i class="fas fa-check-circle"></i>
Fiche d'acteur créée : {{createdActor.name}}
</div>
{{/if}}
<div class="npc-details-grid">
<div class="npc-detail">
<div class="npc-detail-label">Rôle</div>
<div class="npc-detail-value">{{display.roleLabel}}</div>
</div>
<div class="npc-detail">
<div class="npc-detail-label">Catégorie</div>
<div class="npc-detail-value">{{display.categoryLabel}}</div>
</div>
<div class="npc-detail">
<div class="npc-detail-label">Expérience</div>
<div class="npc-detail-value">{{display.experienceLabel}}</div>
</div>
<div class="npc-detail">
<div class="npc-detail-label">Genre</div>
<div class="npc-detail-value">{{display.genderLabel}}</div>
</div>
</div>
<div class="npc-section">
<h4><i class="fas fa-chart-bar"></i> Caractéristiques (UPP: {{upp}})</h4>
<div class="npc-characteristics">
{{#each UPP_ORDER}}
<div class="npc-characteristic">
<div class="npc-char-key">{{lookup ../display.characteristicLabels this}}</div>
<div class="npc-char-value">{{lookup ../characteristics this}}</div>
<div class="npc-char-dm">{{formatDm (lookup ../characteristics this)}}</div>
</div>
{{/each}}
</div>
</div>
{{#if skills}}
<div class="npc-section">
<h4><i class="fas fa-graduation-cap"></i> Compétences</h4>
<div class="npc-skills">
{{#each skills}}
{{#if (gt level 0)}}
<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}}
{{/each}}
</div>
</div>
{{/if}}
<div class="npc-footer">
<small>Généré par le module {{MODULE_ID}}</small>
</div>
</div>