Migration vers le système officiel

This commit is contained in:
2026-05-01 00:57:50 +02:00
parent f31f8aba27
commit 386cf89d68
107 changed files with 1212 additions and 463 deletions
+99 -3
View File
@@ -8,6 +8,7 @@
import { calculatePassengers, calculateCargo, findAvailableGoods, calculatePrice, formatCredits } from './tradeHelper.js';
import { searchWorlds, fetchWorldDetail, fetchWorldCoordinates, calcParsecs } from './travellerMapApi.js';
import { buildActiveActorContext, COMMERCE_SKILLS, getActiveTravellerActor, rollActorSkillEffect, getActorSkillSummary } from './mgt2eSkills.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
@@ -33,9 +34,9 @@ export class CommerceDialog extends FormApplication {
}
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
id: 'mgt2-commerce',
title: 'Commerce MGT2',
title: 'Commerce MgT2e',
template: `modules/${MODULE_ID}/templates/commerce-dialog.hbs`,
width: 780,
height: 'auto',
@@ -51,7 +52,10 @@ export class CommerceDialog extends FormApplication {
getData() {
_registerHandlebarsHelpers();
return foundry.utils.mergeObject(super.getData(), this._formData);
return foundry.utils.mergeObject(super.getData(), {
...this._formData,
activeActor: buildActiveActorContext(),
});
}
activateListeners(html) {
@@ -84,6 +88,31 @@ export class CommerceDialog extends FormApplication {
await this._handleBuyPrices();
});
html.find('[data-action="load-pax-actor"]').on('click', async (ev) => {
ev.preventDefault();
this._applyPassengerActorData(html);
});
html.find('[data-action="roll-pax-effect"]').on('click', async (ev) => {
ev.preventDefault();
await this._rollActorEffectIntoField(html, 'pax.skillEffect', COMMERCE_SKILLS.passengerEffect);
});
html.find('[data-action="load-cargo-actor"]').on('click', async (ev) => {
ev.preventDefault();
this._applyCargoActorData(html);
});
html.find('[data-action="roll-cargo-effect"]').on('click', async (ev) => {
ev.preventDefault();
await this._rollActorEffectIntoField(html, 'cargo.skillEffect', COMMERCE_SKILLS.cargoEffect);
});
html.find('[data-action="load-trade-actor"]').on('click', async (ev) => {
ev.preventDefault();
this._applyTradeActorData(html);
});
// ─── Recherche de monde (Traveller Map API) ───────────────────────────────
html.find('.world-search-widget').each((_, widget) => {
const $widget = $(widget);
@@ -205,6 +234,73 @@ export class CommerceDialog extends FormApplication {
this._formData.trade.blackMarket = html.find('[name="trade.blackMarket"]').is(':checked');
}
_getActiveActorOrWarn() {
const { actor } = getActiveTravellerActor();
if (!actor) {
ui.notifications.warn('Aucun token sélectionné ni personnage assigné pour lire les compétences mgt2e.');
return null;
}
return actor;
}
_applyPassengerActorData(html) {
const actor = this._getActiveActorOrWarn();
if (!actor) return;
const steward = getActorSkillSummary(actor, COMMERCE_SKILLS.steward);
this._setNumericSelectValue(html, 'pax.stewardLevel', steward.value);
this._readForm(html);
ui.notifications.info(`${actor.name} : Intendant ${steward.value} chargé dans le calcul passagers.`);
}
_applyCargoActorData(html) {
const actor = this._getActiveActorOrWarn();
if (!actor) return;
const socMod = Number(actor.system.characteristics?.SOC?.dm ?? 0);
this._setNumericSelectValue(html, 'cargo.socMod', socMod);
this._readForm(html);
ui.notifications.info(`${actor.name} : DM SOC ${socMod >= 0 ? '+' : ''}${socMod} chargé dans le calcul cargaison.`);
}
_applyTradeActorData(html) {
const actor = this._getActiveActorOrWarn();
if (!actor) return;
const broker = getActorSkillSummary(actor, COMMERCE_SKILLS.tradeBroker);
this._setNumericSelectValue(html, 'trade.brokerSkill', broker.value);
this._readForm(html);
ui.notifications.info(`${actor.name} : Courtier ${broker.value} chargé pour le commerce spéculatif.`);
}
async _rollActorEffectIntoField(html, fieldName, skillList) {
const actor = this._getActiveActorOrWarn();
if (!actor) return;
const rollResult = await rollActorSkillEffect(actor, skillList);
if (!rollResult) {
ui.notifications.warn(`${actor.name} ne possède aucune des compétences attendues pour ce calcul.`);
return;
}
this._setNumericSelectValue(html, fieldName, rollResult.effect);
this._readForm(html);
ui.notifications.info(`${actor.name} : ${rollResult.label} → 2D6 (${rollResult.diceTotal}) + ${rollResult.totalModifier >= 0 ? '+' : ''}${rollResult.totalModifier} = ${rollResult.total}, effet ${rollResult.effect >= 0 ? '+' : ''}${rollResult.effect}.`);
}
_setNumericSelectValue(html, fieldName, value) {
const select = html.find(`[name="${fieldName}"]`);
if (!select.length) return;
const normalized = Number(value ?? 0);
if (!select.find(`option[value="${normalized}"]`).length) {
const label = normalized > 0 ? `+${normalized}` : `${normalized}`;
select.append(`<option value="${normalized}">${label}</option>`);
}
select.val(`${normalized}`);
}
// ─── Passagers ─────────────────────────────────────────────────────────────
async _handlePassengers() {
+1 -1
View File
@@ -25,7 +25,7 @@ export class NpcDialog extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: 'mgt2-npc',
title: 'PNJ & Rencontres MGT2',
title: 'PNJ & Rencontres MgT2e',
template: `modules/${MODULE_ID}/templates/npc-dialog.hbs`,
width: 720,
height: 'auto',
+1
View File
@@ -6,6 +6,7 @@
*/
import { CommerceDialog } from './CommerceDialog.js';
import './mgt2eMigration.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
+8 -8
View File
@@ -25,7 +25,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Non-combattant bleu',
skillLevel: 0,
characteristicBonuses: [],
skills: ['Conduire/aéronef'],
skills: ['pilot'],
},
{
key: 'combatant-blue',
@@ -34,7 +34,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Combattant bleu',
skillLevel: 0,
characteristicBonuses: [],
skills: ['Conduire/aéronef', 'Combat Arme', 'Mêlée'],
skills: ['pilot', 'guncombat', 'melee'],
},
{
key: 'noncombatant-average',
@@ -43,7 +43,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Non-combattant moyen',
skillLevel: 1,
characteristicBonuses: ['+1'],
skills: ['Conduire/aéronef', 'Profession'],
skills: ['pilot', 'profession'],
},
{
key: 'combatant-average',
@@ -52,7 +52,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Combattant moyen',
skillLevel: 1,
characteristicBonuses: ['+1'],
skills: ['Conduire/aéronef', 'Combat Arme', 'Mêlée', 'Reconnaissance'],
skills: ['pilot', 'guncombat', 'melee', 'recon'],
},
{
key: 'noncombatant-experienced',
@@ -61,7 +61,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Non-combattant expérimenté',
skillLevel: 2,
characteristicBonuses: ['+1', '+2'],
skills: ['Administration', 'Conduire/aéronef', 'Profession'],
skills: ['admin', 'pilot', 'profession'],
},
{
key: 'combatant-experienced',
@@ -70,7 +70,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Combattant expérimenté',
skillLevel: 2,
characteristicBonuses: ['+1', '+2'],
skills: ['Conduire/aéronef', 'Combat Arme', 'Armes lourdes', 'Mêlée', 'Reconnaissance'],
skills: ['pilot', 'guncombat', 'heavyweapons', 'melee', 'recon'],
},
{
key: 'noncombatant-elite',
@@ -79,7 +79,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Non-combattant élite',
skillLevel: 3,
characteristicBonuses: ['+1', '+2', '+3'],
skills: ['Administration', 'Conduire/aéronef', 'Enquêter', 'Profession'],
skills: ['admin', 'pilot', 'investigate', 'profession'],
},
{
key: 'combatant-elite',
@@ -88,7 +88,7 @@ export const EXPERIENCE_PROFILES = [
label: 'Combattant élite',
skillLevel: 3,
characteristicBonuses: ['+1', '+2', '+3'],
skills: ['Conduire/aéronef', 'Combat Arme', 'Armes lourdes', 'Mêlée', 'Reconnaissance', 'Tactique'],
skills: ['pilot', 'guncombat', 'heavyweapons', 'melee', 'recon', 'tactics'],
},
];
+406
View File
@@ -0,0 +1,406 @@
import { inferWeaponSkillFromName } from './mgt2eSkills.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const MIGRATION_SETTING = 'mgt2eMigrationVersion';
const MIGRATION_VERSION = 2;
const ITEM_PACKS = [
'armures',
'competences',
'maladie-poison-and-blessure',
'objet',
'equipement',
'ordinateur',
'contenant-sac-coffre',
'espece',
'armes',
'talents-psioniques',
'carrieres',
];
Hooks.once('init', () => {
game.settings.register(MODULE_ID, MIGRATION_SETTING, {
scope: 'world',
config: false,
type: Number,
default: 0,
});
});
Hooks.once('ready', async () => {
if (game.system?.id !== 'mgt2e' || !game.user?.isGM) return;
const currentVersion = game.settings.get(MODULE_ID, MIGRATION_SETTING) ?? 0;
if (currentVersion >= MIGRATION_VERSION) return;
ui.notifications.info('Migration mgt2e du module en cours...');
try {
for (const packName of ITEM_PACKS) {
await migrateItemPack(packName);
}
await game.settings.set(MODULE_ID, MIGRATION_SETTING, MIGRATION_VERSION);
ui.notifications.info('Migration mgt2e du module terminée.');
} catch (error) {
console.error(`${MODULE_ID} | Migration mgt2e impossible`, error);
ui.notifications.error(`Migration mgt2e inachevee : ${error.message}`);
}
});
async function migrateItemPack(packName) {
const collection = `${MODULE_ID}.${packName}`;
const pack = game.packs.get(collection);
if (!pack || pack.documentName !== 'Item') return;
const originalLocked = pack.locked;
if (originalLocked) {
await pack.configure({ locked: false });
}
try {
const documents = await pack.getDocuments();
const folderNames = buildFolderNameMap(pack);
const failures = [];
for (const document of documents) {
const folderId = document.folder?.id ?? document.folder ?? null;
const update = buildMigratedItemData(document, packName, folderNames.get(folderId));
if (!update) continue;
try {
await document.update(update, { diff: false, recursive: false });
} catch (error) {
failures.push(`${document.name}: ${error.message}`);
}
}
if (failures.length) {
throw new Error(`${packName} (${failures.length} erreurs) — ${failures.slice(0, 5).join(' | ')}`);
}
} finally {
if (originalLocked) {
await pack.configure({ locked: true });
}
}
}
function buildFolderNameMap(pack) {
const folders = new Map();
for (const folder of pack.folders ?? []) {
folders.set(folder.id, folder.name ?? '');
}
return folders;
}
function buildMigratedItemData(document, packName, folderName = '') {
const legacyType = document.type;
const folderId = document.folder?.id ?? document.folder ?? null;
const common = {
name: document.name,
img: document.img,
flags: foundry.utils.mergeObject(document.flags ?? {}, {
[MODULE_ID]: {
legacyType,
migratedToMgt2e: MIGRATION_VERSION,
},
}),
folder: folderId,
sort: document.sort,
ownership: document.ownership,
};
switch (legacyType) {
case 'armor':
return {
...common,
type: 'armour',
system: buildArmourSystem(document),
};
case 'weapon':
return {
...common,
type: 'weapon',
system: buildWeaponSystem(document),
};
case 'equipment':
return {
...common,
type: isAugmentFolder(folderName) ? 'augment' : 'item',
system: isAugmentFolder(folderName) ? buildAugmentSystem(document) : buildSimpleItemSystem(document),
};
case 'computer':
return {
...common,
type: 'hardware',
system: buildHardwareSystem(document),
};
case 'item':
if (document.system?.subType === 'software') {
return {
...common,
type: 'software',
system: buildSoftwareSystem(document),
};
}
return {
...common,
type: 'item',
system: buildSimpleItemSystem(document),
};
case 'career':
return {
...common,
type: 'item',
system: buildReferenceItemSystem(document, 'Carriere', packName),
};
case 'talent':
return {
...common,
type: 'item',
system: buildReferenceItemSystem(document, document.system?.subType === 'psionic' ? 'Talent psionique' : 'Competence', packName),
};
case 'disease':
return {
...common,
type: 'item',
system: buildDiseaseItemSystem(document),
};
default:
return {
...common,
type: 'item',
system: buildReferenceItemSystem(document, legacyType, packName),
};
}
}
function buildWeaponSystem(document) {
const system = document.system ?? {};
const traits = normalizeTraits(system.traits);
const melee = Boolean(system.range?.isMelee);
return {
tl: normalizeTl(system.tl),
weight: toNumber(system.weight),
cost: toNumber(system.cost),
notes: traits.notes,
active: Boolean(system.equipped),
quantity: toNumber(system.quantity, 1),
status: system.trash ? 'broken' : null,
legality: 9,
weapon: {
scale: 'traveller',
range: melee ? 0 : toNumber(system.range?.value),
minRange: 0,
damage: normalizeDamage(system.damage),
magazine: toNumber(system.magazine),
ammo: toNumber(system.magazine),
magazineCost: toNumber(system.magazineCost),
characteristic: melee ? 'STR' : 'DEX',
skill: inferWeaponSkillFromName(document.name, melee),
parryBonus: 0,
damageBonus: melee ? 'STR' : '',
damageType: 'standard',
attackBonus: 0,
traits: traits.names,
},
description: formatDescription(system.description),
};
}
function buildArmourSystem(document) {
const system = document.system ?? {};
return {
tl: normalizeTl(system.tl),
weight: toNumber(system.weight),
cost: toNumber(system.cost),
notes: buildLines([
system.requireSkillLevel != null ? `Combi requise: ${system.requireSkillLevel}` : '',
]),
active: Boolean(system.equipped),
quantity: toNumber(system.quantity, 1),
status: system.trash ? 'broken' : null,
legality: 9,
armour: {
protection: toNumber(String(system.protection ?? '').replace(/[^\d-]/g, '')),
otherProtection: 0,
otherTypes: '',
rad: toNumber(system.radiations),
archaic: 0,
skill: toNumber(system.requireSkillLevel) > 0 ? 'vaccsuit' : '',
duration: 0,
slots: 0,
form: 'standard',
layered: 0,
ablat: 0,
powered: Boolean(system.powered) ? 1 : 0,
psi: 0,
worn: Boolean(system.equipped) ? 1 : 0,
},
description: formatDescription(system.description),
};
}
function buildSimpleItemSystem(document) {
const system = document.system ?? {};
return {
tl: normalizeTl(system.tl),
weight: toNumber(system.weight),
cost: toNumber(system.cost),
notes: '',
active: Boolean(system.equipped),
quantity: toNumber(system.quantity, 1),
status: system.trash ? 'broken' : null,
legality: 9,
description: formatDescription(system.description),
};
}
function buildAugmentSystem(document) {
const system = buildSimpleItemSystem(document);
return system;
}
function buildHardwareSystem(document) {
const system = document.system ?? {};
return {
tl: normalizeTl(system.tl),
weight: toNumber(system.weight),
cost: toNumber(system.cost),
notes: buildLines([
system.overload ? 'Surcharge: oui' : '',
system.processing != null ? `Traitement: ${system.processing}` : '',
system.processingUsed != null ? `Traitement utilise: ${system.processingUsed}` : '',
]),
active: false,
quantity: toNumber(system.quantity, 1),
status: system.trash ? 'broken' : null,
legality: 9,
hardware: {
system: 'computer',
tons: 0,
power: toNumber(system.processing),
rating: inferRating(document.name),
variables: {
max: 0,
tl: normalizeTl(system.tl),
cost: toNumber(system.cost),
},
},
description: formatDescription(system.description),
};
}
function buildSoftwareSystem(document) {
const system = document.system ?? {};
return {
quantity: toNumber(system.quantity, 1),
tl: normalizeTl(system.tl),
software: {
bandwidth: toNumber(system.software?.bandwidth),
},
cost: toNumber(system.cost),
description: formatDescription(system.description),
};
}
function buildReferenceItemSystem(document, category, packName) {
const system = document.system ?? {};
const isSkillReference = packName === 'competences';
return {
tl: normalizeTl(system.tl),
weight: toNumber(system.weight),
cost: toNumber(system.cost),
notes: buildLines([
isSkillReference ? 'Référence documentaire uniquement — les compétences de jeu utilisent désormais actor.system.skills et la localisation MGT2.Skills.*.' : '',
category ? `Categorie legacy: ${category}` : '',
system.subType ? `Sous-type legacy: ${system.subType}` : '',
system.level != null ? `Niveau legacy: ${system.level}` : '',
system.assignment ? `Affectations: ${system.assignment}` : '',
]),
active: false,
quantity: toNumber(system.quantity, 1),
status: null,
legality: 9,
description: isSkillReference
? formatDescription([system.description, '<p><strong>Note mgt2e :</strong> cette entrée sert de référence documentaire. Les compétences jouables sont natives à la fiche de personnage `mgt2e`.</p>'].filter(Boolean).join('\n\n'))
: formatDescription(system.description),
};
}
function buildDiseaseItemSystem(document) {
const system = document.system ?? {};
return {
tl: '0',
weight: 0,
cost: 0,
notes: buildLines([
system.subType ? `Sous-type: ${system.subType}` : '',
system.difficulty ? `Difficulte: ${system.difficulty}` : '',
system.damage ? `Degats: ${system.damage}` : '',
system.interval ? `Intervalle: ${system.interval}` : '',
]),
active: false,
quantity: 1,
status: null,
legality: 9,
description: formatDescription(system.description),
};
}
function isAugmentFolder(folderName) {
return /augmentation/i.test(folderName ?? '');
}
function normalizeTl(value) {
const match = String(value ?? '').match(/(\d+)/);
return match ? match[1] : '0';
}
function normalizeDamage(value) {
return String(value ?? '1D6').replace(/d/g, 'D');
}
function toNumber(value, fallback = 0) {
const parsed = Number.parseInt(String(value ?? ''), 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function inferRating(name) {
const match = String(name ?? '').match(/\/(\d+)/);
return match ? Number.parseInt(match[1], 10) : 0;
}
function normalizeTraits(traits) {
if (!Array.isArray(traits) || !traits.length) return { names: '', notes: '' };
const names = traits
.map((trait) => trait?.name ?? trait)
.filter(Boolean)
.join(', ');
const notes = traits
.filter((trait) => trait?.description)
.map((trait) => `${trait.name}: ${trait.description}`)
.join('\n');
return { names, notes };
}
function formatDescription(value) {
const text = String(value ?? '').trim();
if (!text) return '';
if (text.includes('<')) return text;
const paragraphs = text
.split(/\n{2,}/)
.map((part) => part.trim())
.filter(Boolean)
.map((part) => `<p>${part.replace(/\n/g, '<br>')}</p>`);
return paragraphs.join('');
}
function buildLines(lines) {
return lines.filter(Boolean).join('\n');
}
+180
View File
@@ -0,0 +1,180 @@
const SKILL_LOCALIZATION_PREFIX = 'MGT2.Skills.';
export const COMMERCE_SKILLS = Object.freeze({
passengerEffect: ['carouse', 'broker', 'streetwise'],
cargoEffect: ['broker', 'streetwise'],
tradeBroker: 'broker',
steward: 'steward',
});
export function splitSkillFqn(skillFqn = '') {
const [skillId = '', specialityId = ''] = String(skillFqn ?? '').split('.');
return { skillId, specialityId };
}
export function localizeSkill(skillFqn, fallback = '') {
const { skillId, specialityId } = splitSkillFqn(skillFqn);
if (!skillId) return fallback;
const skillLabel = localizeSkillId(skillId);
if (!specialityId) return skillLabel;
return `${skillLabel} (${localizeSkillId(specialityId)})`;
}
export function getActiveTravellerActor() {
const controlled = canvas?.tokens?.controlled?.find((token) => token.actor?.type === 'traveller' || token.actor?.type === 'npc')?.actor;
if (controlled) return { actor: controlled, source: 'token' };
if (game.user?.character?.type === 'traveller' || game.user?.character?.type === 'npc') {
return { actor: game.user.character, source: 'character' };
}
const owned = game.actors?.find((actor) => actor.isOwner && (actor.type === 'traveller' || actor.type === 'npc'));
if (owned) return { actor: owned, source: 'owned' };
return { actor: null, source: null };
}
export function getActorSkillSummary(actor, skillFqn) {
if (!actor?.system?.skills) return buildEmptySkillSummary(skillFqn);
const { skillId, specialityId } = splitSkillFqn(skillFqn);
const skill = actor.system.skills[skillId];
if (!skill) return buildEmptySkillSummary(skillFqn);
const speciality = specialityId ? skill.specialities?.[specialityId] ?? null : null;
const characteristic = speciality?.default || skill.default || null;
const characteristicDm = characteristic ? Number(actor.system.characteristics?.[characteristic]?.dm ?? 0) : 0;
const trained = Boolean(skill.trained) || Number(skill.value ?? 0) > 0 || Number(speciality?.value ?? 0) > 0;
const value = speciality ? Number(speciality.value ?? 0) : Number(skill.value ?? 0);
const jackOfAllTrades = Number(actor.system.skills?.jackofalltrades?.value ?? 0);
const rollValue = trained ? value : jackOfAllTrades - 3;
return {
skillFqn,
skillId,
specialityId,
label: localizeSkill(skillFqn, skill.label || skillId),
value,
rollValue,
trained,
characteristic,
characteristicDm,
totalModifier: rollValue + characteristicDm,
};
}
export function getActorCharacteristicSummary(actor, characteristicId) {
if (!actor?.system?.characteristics?.[characteristicId]) {
return { id: characteristicId, value: 0, dm: 0, label: characteristicId };
}
const characteristic = actor.system.characteristics[characteristicId];
return {
id: characteristicId,
value: Number(characteristic.value ?? 0),
dm: Number(characteristic.dm ?? 0),
label: game.i18n.localize(`MGT2.Characteristics.${characteristicId}`),
};
}
export function getBestActorSkillSummary(actor, skillList = []) {
const summaries = skillList
.map((skillFqn) => getActorSkillSummary(actor, skillFqn))
.filter((summary) => summary.skillId);
if (!summaries.length) return null;
return summaries.sort((left, right) => {
if (right.totalModifier !== left.totalModifier) return right.totalModifier - left.totalModifier;
if (right.value !== left.value) return right.value - left.value;
return left.label.localeCompare(right.label, 'fr');
})[0];
}
export async function rollActorSkillEffect(actor, skillList = [], difficulty = 8) {
const summary = getBestActorSkillSummary(actor, skillList);
if (!summary) return null;
const roll = await new Roll('2d6').evaluate();
const total = Number(roll.total ?? 0) + summary.totalModifier;
return {
...summary,
diceTotal: Number(roll.total ?? 0),
total,
effect: total - difficulty,
difficulty,
};
}
export function setSkillLevel(skills, skillFqn, level) {
const { skillId, specialityId } = splitSkillFqn(skillFqn);
if (!skillId || !skills?.[skillId]) return skills;
const numericLevel = Number(level ?? 0);
const skill = foundry.utils.mergeObject(skills[skillId], { trained: numericLevel > 0 || skills[skillId].trained });
if (specialityId && skill.specialities?.[specialityId]) {
skill.specialities[specialityId] = foundry.utils.mergeObject(skill.specialities[specialityId], {
value: Math.max(Number(skill.specialities[specialityId].value ?? 0), numericLevel),
});
} else {
skill.value = Math.max(Number(skill.value ?? 0), numericLevel);
}
skills[skillId] = skill;
return skills;
}
export function buildActiveActorContext() {
const { actor, source } = getActiveTravellerActor();
if (!actor) return null;
return {
id: actor.id,
name: actor.name,
source,
sourceLabel: source === 'token' ? 'token sélectionné' : source === 'character' ? 'personnage assigné' : 'acteur possédé',
broker: getActorSkillSummary(actor, 'broker'),
carouse: getActorSkillSummary(actor, 'carouse'),
streetwise: getActorSkillSummary(actor, 'streetwise'),
steward: getActorSkillSummary(actor, 'steward'),
soc: getActorCharacteristicSummary(actor, 'SOC'),
};
}
export function inferWeaponSkillFromName(name, melee) {
const label = String(name ?? '').toLowerCase();
if (melee) {
if (/(sabre|épée|epee|lame|poignard|couteau|fleuret|rapiere|rapière)/i.test(label)) return 'melee.blade';
if (/(massue|matraque|bâton|baton|gourdin|marteau|masse)/i.test(label)) return 'melee.bludgeon';
if (/(griffe|morsure|corne|tentacule|naturel)/i.test(label)) return 'melee.natural';
return 'melee.unarmed';
}
if (/(laser|plasma|fusion|particule|meson)/i.test(label)) return 'guncombat.energy';
if (/(arc|arbal[eè]te|javelot|lance[- ]?harpon)/i.test(label)) return 'guncombat.archaic';
if (/(canon|lance[- ]?grenade|missile|mortier|roquette)/i.test(label)) return 'heavyweapons.portable';
return 'guncombat.slug';
}
function localizeSkillId(skillId) {
const localized = game.i18n.localize(`${SKILL_LOCALIZATION_PREFIX}${skillId}`);
return localized.startsWith(SKILL_LOCALIZATION_PREFIX) ? skillId : localized;
}
function buildEmptySkillSummary(skillFqn) {
const { skillId, specialityId } = splitSkillFqn(skillFqn);
return {
skillFqn,
skillId,
specialityId,
label: localizeSkill(skillFqn, skillId),
value: 0,
rollValue: -3,
trained: false,
characteristic: null,
characteristicDm: 0,
totalModifier: -3,
};
}
+1
View File
@@ -1,5 +1,6 @@
import { NpcDialog } from './NpcDialog.js';
import { syncNpcRollTables } from './npcRollTableSync.js';
import './mgt2eMigration.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
+106 -94
View File
@@ -10,31 +10,40 @@ import {
RANDOM_OPPOSITION_TABLE,
ENCOUNTER_CONTEXTS,
} from './data/npcTables.js';
import { localizeSkill, setSkillLevel } from './mgt2eSkills.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const SKILL_PACK_ID = `${MODULE_ID}.competences`;
const CORE_CHARACTERISTICS = ['strength', 'dexterity', 'endurance', 'intellect', 'education', 'social'];
const MGT2E_CHARACTERISTICS = {
strength: 'STR',
dexterity: 'DEX',
endurance: 'END',
intellect: 'INT',
education: 'EDU',
social: 'SOC',
};
const DEFAULT_PRIORITIES = {
'Non-combattant': ['intellect', 'education', 'social', 'dexterity', 'endurance', 'strength'],
'Combattant': ['dexterity', 'endurance', 'strength', 'education', 'intellect', 'social'],
};
let mgt2eBaseActorSystemPromise = null;
const ROLE_HINTS = [
{ match: /médecin/i, skills: ['Médecine', 'Science'], priorities: ['education', 'intellect', 'dexterity'] },
{ match: /scientifique|chercheur/i, skills: ['Science', 'Électronique'], priorities: ['intellect', 'education', 'dexterity'] },
{ match: /diplomate|ambassadeur|attaché culturel/i, skills: ['Diplomatie', 'Langage', 'Persuader'], priorities: ['social', 'education', 'intellect'] },
{ match: /marchand|franc-marchand|courtier/i, skills: ['Courtier', 'Administration', 'Persuader'], priorities: ['social', 'education', 'intellect'] },
{ match: /mercenaire|seigneur de guerre/i, skills: ['Combat Arme', 'Mêlée', 'Reconnaissance'], priorities: ['dexterity', 'endurance', 'strength'] },
{ match: /officier de marine|amiral|capitaine/i, skills: ['Leadership', 'Marin', 'Tactique'], priorities: ['education', 'social', 'intellect'] },
{ match: /explorateur|éclaireur/i, skills: ['Reconnaissance', 'Survie', 'Navigation'], priorities: ['education', 'dexterity', 'endurance'] },
{ match: /interprète|xéno/i, skills: ['Langage', 'Diplomatie', 'Science'], priorities: ['education', 'intellect', 'social'] },
{ match: /cadre de corpo|agent corpo|administrateur|gouverneur|homme d'état|noble/i, skills: ['Administration', 'Leadership', 'Diplomatie'], priorities: ['social', 'education', 'intellect'] },
{ match: /journaliste|enquêteur|inspecteur|agent impérial/i, skills: ['Enquêter', 'Persuader', 'Combat Arme'], priorities: ['intellect', 'education', 'dexterity'] },
{ match: /conspirateur|criminel|contrebandier/i, skills: ['Duperie', 'Sens de la rue', 'Combat Arme'], priorities: ['social', 'dexterity', 'intellect'] },
{ match: /chef religieux|cultiste/i, skills: ['Persuader', 'Leadership', 'Langage'], priorities: ['social', 'education', 'intellect'] },
{ match: /joueur|playboy/i, skills: ['Flambeur', 'Mondanités', 'Persuader'], priorities: ['social', 'intellect', 'education'] },
{ match: /intelligence artificielle/i, skills: ['Électronique', 'Science', 'Profession'], priorities: ['intellect', 'education', 'social'] },
{ match: /médecin/i, skills: ['medic', 'science'], priorities: ['education', 'intellect', 'dexterity'] },
{ match: /scientifique|chercheur/i, skills: ['science', 'electronics'], priorities: ['intellect', 'education', 'dexterity'] },
{ match: /diplomate|ambassadeur|attaché culturel/i, skills: ['diplomat', 'language', 'persuade'], priorities: ['social', 'education', 'intellect'] },
{ match: /marchand|franc-marchand|courtier/i, skills: ['broker', 'admin', 'persuade'], priorities: ['social', 'education', 'intellect'] },
{ match: /mercenaire|seigneur de guerre/i, skills: ['guncombat', 'melee', 'recon'], priorities: ['dexterity', 'endurance', 'strength'] },
{ match: /officier de marine|amiral|capitaine/i, skills: ['leadership', 'seafarer', 'tactics'], priorities: ['education', 'social', 'intellect'] },
{ match: /explorateur|éclaireur/i, skills: ['recon', 'survival', 'navigation'], priorities: ['education', 'dexterity', 'endurance'] },
{ match: /interprète|xéno/i, skills: ['language', 'diplomat', 'science'], priorities: ['education', 'intellect', 'social'] },
{ match: /cadre de corpo|agent corpo|administrateur|gouverneur|homme d'état|noble/i, skills: ['admin', 'leadership', 'diplomat'], priorities: ['social', 'education', 'intellect'] },
{ match: /journaliste|enquêteur|inspecteur|agent impérial/i, skills: ['investigate', 'persuade', 'guncombat'], priorities: ['intellect', 'education', 'dexterity'] },
{ match: /conspirateur|criminel|contrebandier/i, skills: ['deception', 'streetwise', 'guncombat'], priorities: ['social', 'dexterity', 'intellect'] },
{ match: /chef religieux|cultiste/i, skills: ['persuade', 'leadership', 'language'], priorities: ['social', 'education', 'intellect'] },
{ match: /joueur|playboy/i, skills: ['gambler', 'carouse', 'persuade'], priorities: ['social', 'intellect', 'education'] },
{ match: /intelligence artificielle/i, skills: ['electronics', 'science', 'profession'], priorities: ['intellect', 'education', 'social'] },
];
function getD66Entry(entries, total) {
@@ -126,9 +135,9 @@ async function generateExperience(mode = 'random') {
}
function findRoleHint(roleName, category) {
const hint = ROLE_HINTS.find((entry) => entry.match.test(roleName));
const hint = ROLE_HINTS.find((entry) => entry.match.test(roleName));
return hint ?? {
skills: ['Profession'],
skills: ['profession'],
priorities: DEFAULT_PRIORITIES[category] ?? DEFAULT_PRIORITIES['Non-combattant'],
};
}
@@ -158,41 +167,6 @@ function buildCharacteristicValues(result) {
};
}
function buildCharacteristicsData(values) {
const allKeys = {
strength: { showMax: true },
dexterity: { showMax: true },
endurance: { showMax: true },
intellect: { showMax: false },
education: { showMax: false },
social: { showMax: false },
morale: { showMax: false, value: 0 },
luck: { showMax: false, value: 0 },
sanity: { showMax: false, value: 0 },
charm: { showMax: false, value: 0 },
psionic: { showMax: false, value: 0 },
other: { showMax: false, value: 0 },
};
return Object.fromEntries(Object.entries(allKeys).map(([key, config]) => {
const value = values[key] ?? config.value ?? 0;
return [key, {
value,
max: value,
dm: calculateDm(value),
show: true,
showMax: config.showMax,
}];
}));
}
async function getSkillPackIndex() {
const pack = game.packs.get(SKILL_PACK_ID);
if (!pack) throw new Error(`Pack de compétences introuvable : ${SKILL_PACK_ID}`);
const index = await pack.getIndex();
return { pack, index };
}
function mergeSkillLevels(profileSkills, roleSkills, baseLevel) {
const levels = new Map();
@@ -202,36 +176,69 @@ function mergeSkillLevels(profileSkills, roleSkills, baseLevel) {
return levels;
}
async function buildSkillItems(result) {
const { pack, index } = await getSkillPackIndex();
const { hint } = buildCharacteristicValues(result);
const skillLevels = mergeSkillLevels(result.experience.profile.skills, hint.skills, result.experience.profile.skillLevel);
const items = [];
function buildMgt2eCharacteristics(existingCharacteristics = {}, values) {
const characteristics = foundry.utils.deepClone(existingCharacteristics);
for (const [skillName, level] of skillLevels.entries()) {
const entry = index.contents.find((item) => item.name === skillName);
if (!entry) continue;
const document = await pack.getDocument(entry._id);
const data = document.toObject();
delete data._id;
delete data.folder;
data.system.level = level;
items.push(data);
for (const [legacyKey, targetKey] of Object.entries(MGT2E_CHARACTERISTICS)) {
const value = values[legacyKey] ?? 7;
characteristics[targetKey] = foundry.utils.mergeObject(characteristics[targetKey] ?? {}, {
value,
current: value,
dm: calculateDm(value),
show: true,
default: false,
});
}
return items;
return characteristics;
}
function buildMgt2eSkills(existingSkills = {}, result) {
const skills = foundry.utils.deepClone(existingSkills);
const { hint } = buildCharacteristicValues(result);
const skillLevels = mergeSkillLevels(result.experience.profile.skills, hint.skills, result.experience.profile.skillLevel);
for (const [skillFqn, level] of skillLevels.entries()) {
setSkillLevel(skills, skillFqn, level);
}
return skills;
}
function buildActorDescription(result, actorName, ucp) {
const { hint } = buildCharacteristicValues(result);
const notableSkills = mergeSkillLevels(result.experience.profile.skills, hint.skills, result.experience.profile.skillLevel);
return [
`${actorName}${result.role.entry.text}`,
`Relation : ${result.relation.label}`,
`Particularité : ${result.quirk.entry.text}`,
`Expérience : ${result.experience.profile.label}`,
`UCP : ${ucp}`,
`UPP : ${ucp}`,
`Compétences clés : ${Array.from(notableSkills.keys()).map((skill) => localizeSkill(skill)).join(', ')}`,
].join('\n');
}
async function getMgt2eBaseActorSystem() {
if (!mgt2eBaseActorSystemPromise) {
mgt2eBaseActorSystemPromise = (async () => {
const pack = game.packs.get('mgt2e.base-actors');
if (!pack) return null;
const index = Array.from(await pack.getIndex({ fields: ['name', 'type'] }));
const entry = index.find((document) => document.name === 'DEFAULT TRAVELLER')
?? index.find((document) => document.type === 'traveller')
?? index[0];
if (!entry?._id) return null;
const document = await pack.getDocument(entry._id);
return document?.toObject()?.system ?? null;
})();
}
const system = await mgt2eBaseActorSystemPromise;
return system ? foundry.utils.deepClone(system) : null;
}
export function formatSigned(value) {
return value >= 0 ? `+${value}` : `${value}`;
}
@@ -254,32 +261,27 @@ export async function generateQuickNpc(params = {}) {
}
export async function createNpcActor(result, options = {}) {
const actorName = options.name?.trim() || `PNJ — ${result.role.entry.text}`;
const requestedName = options.name?.trim();
const { values, ucp } = buildCharacteristicValues(result);
const items = await buildSkillItems(result);
const actor = await Actor.create({
name: actorName,
type: 'character',
img: 'icons/svg/mystery-man.svg',
const baseActorSystem = game.system?.id === 'mgt2e' ? await getMgt2eBaseActorSystem() : null;
const npcData = {
name: requestedName || `PNJ — ${result.role.entry.text}`,
type: 'npc',
img: 'systems/mgt2e/icons/cargo/passenger-middle.svg',
system: {
life: {
value: values.endurance,
max: values.endurance,
},
personal: {
title: result.role.entry.text,
species: '',
speciesText: {},
age: '',
ucp,
traits: [
{ name: result.relation.label },
{ name: result.quirk.entry.text },
],
},
characteristics: buildCharacteristicsData(values),
biography: buildActorDescription(result, actorName, ucp),
notes: buildActorDescription(result, actorName, ucp),
settings: foundry.utils.mergeObject(foundry.utils.deepClone(baseActorSystem?.settings ?? {}), {
hideUntrained: true,
lockCharacteristics: true,
}),
sophont: foundry.utils.mergeObject(foundry.utils.deepClone(baseActorSystem?.sophont ?? {}), {
age: 18,
homeworld: '',
profession: result.role.entry.text,
}),
characteristics: foundry.utils.deepClone(baseActorSystem?.characteristics ?? {}),
hits: foundry.utils.deepClone(baseActorSystem?.hits ?? {}),
skills: foundry.utils.deepClone(baseActorSystem?.skills ?? {}),
description: '',
},
flags: {
[MODULE_ID]: {
@@ -288,12 +290,22 @@ export async function createNpcActor(result, options = {}) {
role: result.role.entry.text,
quirk: result.quirk.entry.text,
experience: result.experience.profile.label,
upp: ucp,
},
},
},
}, { renderSheet: false });
};
npcData.name = requestedName || npcData.name || `PNJ — ${result.role.entry.text}`;
npcData.system.sophont = foundry.utils.mergeObject(npcData.system.sophont ?? {}, {
profession: result.role.entry.text,
});
npcData.system.characteristics = buildMgt2eCharacteristics(npcData.system.characteristics, values);
npcData.system.skills = buildMgt2eSkills(npcData.system.skills, result);
npcData.system.description = buildActorDescription(result, npcData.name, ucp).replace(/\n/g, '<br>');
const actor = await Actor.create(npcData, { renderSheet: false });
if (items.length) await actor.createEmbeddedDocuments('Item', items);
if (options.openSheet !== false) actor.sheet?.render(true);
return actor;
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { NPC_ROLLTABLE_DEFINITIONS } from './data/npcTables.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const PACK_ID = `${MODULE_ID}.tables-pnj`;
const WORLD_FOLDER_NAME = 'MGT2 — Tables PNJ';
const WORLD_FOLDER_NAME = 'MgT2e — Tables PNJ';
const ROLLTABLES_VERSION = 1;
function entryText(entry) {