181 lines
6.4 KiB
JavaScript
181 lines
6.4 KiB
JavaScript
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,
|
|
};
|
|
}
|