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, }; }