270 lines
6.9 KiB
JavaScript
270 lines
6.9 KiB
JavaScript
import DG from "../config/index.js";
|
|
import { AGENT_SKILL_DEFAULTS } from "../data/actor/base/agent-skill-defaults.js";
|
|
import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js";
|
|
import {
|
|
INTERNAL_TYPED_KEY_PATTERN,
|
|
TYPED_KEY_PATTERN,
|
|
TYPED_GROUP_I18N,
|
|
} from "./constants.js";
|
|
/**
|
|
* @returns {string}
|
|
*/
|
|
export function getChooseOnePlaceholderLabel() {
|
|
return game.i18n.localize("DG.Profession.ChooseOnePlaceholder");
|
|
}
|
|
|
|
/**
|
|
* @param {string} label
|
|
* @returns {string}
|
|
*/
|
|
export function normalizeTypedSkillName(label) {
|
|
return label.trim().toLocaleLowerCase(game.i18n.lang);
|
|
}
|
|
|
|
/**
|
|
* @param {string} label
|
|
* @returns {boolean}
|
|
*/
|
|
export function isChooseOnePlaceholderLabel(label) {
|
|
return (
|
|
normalizeTypedSkillName(label) ===
|
|
normalizeTypedSkillName(getChooseOnePlaceholderLabel())
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {string} mapKey
|
|
* @param {Record<string, ProfessionSkillMeta>} automaticMeta
|
|
* @param {Record<string, ProfessionSkillMeta>} optionMeta
|
|
* @returns {boolean}
|
|
*/
|
|
export function isChooseOneProfessionSkillKey(
|
|
mapKey,
|
|
automaticMeta = {},
|
|
optionMeta = {},
|
|
) {
|
|
return Boolean(
|
|
automaticMeta[mapKey]?.chooseOne ?? optionMeta[mapKey]?.chooseOne,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {object} params
|
|
* @param {string} params.group
|
|
* @param {string} params.label
|
|
* @param {boolean} params.chooseOne
|
|
* @param {Record<string, number>} params.automaticSkills
|
|
* @param {Record<string, number>} params.optionSkills
|
|
* @param {Record<string, ProfessionSkillMeta>} [params.automaticMeta]
|
|
* @param {Record<string, ProfessionSkillMeta>} [params.optionMeta]
|
|
* @param {string} [params.excludeKey]
|
|
* @returns {string | null} Conflicting map key, if any.
|
|
*/
|
|
export function findTypedSkillNameConflict({
|
|
group,
|
|
label,
|
|
chooseOne,
|
|
automaticSkills,
|
|
optionSkills,
|
|
automaticMeta = {},
|
|
optionMeta = {},
|
|
excludeKey = null,
|
|
}) {
|
|
// Multiple choose-one slots per category are allowed on the profession item.
|
|
if (chooseOne) return null;
|
|
if (!group) return null;
|
|
|
|
const normalizedGroup = normalizeTypedGroup(group);
|
|
const normalizedNew = normalizeTypedSkillName(label);
|
|
if (!normalizedNew) return "__empty__";
|
|
|
|
const maps = [
|
|
[automaticSkills, automaticMeta],
|
|
[optionSkills, optionMeta],
|
|
];
|
|
|
|
for (const [skillMap, metaMap] of maps) {
|
|
for (const mapKey of Object.keys(skillMap ?? {})) {
|
|
if (mapKey === excludeKey || mapKey === PROFESSION_OPTION_PICKS_KEY)
|
|
continue;
|
|
|
|
const ref = parseProfessionSkillKey(mapKey);
|
|
if (!ref || ref.kind !== "typed") continue;
|
|
if (normalizeTypedGroup(ref.group) !== normalizedGroup) continue;
|
|
|
|
// Unresolved choose-one slots have no fixed name yet.
|
|
if (metaMap[mapKey]?.chooseOne) continue;
|
|
|
|
if (normalizeTypedSkillName(ref.label) === normalizedNew) return mapKey;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @returns {Record<string, number>}
|
|
*/
|
|
export function getAgentSkillDefaults() {
|
|
return { ...AGENT_SKILL_DEFAULTS };
|
|
}
|
|
|
|
/**
|
|
* @param {string} group
|
|
* @returns {string}
|
|
*/
|
|
export function normalizeTypedGroup(group) {
|
|
if (group == null) return "";
|
|
const trimmed = String(group).trim();
|
|
const map = {
|
|
"Foreign Language": "ForeignLanguage",
|
|
"Military Science": "MilitaryScience",
|
|
};
|
|
return map[trimmed] ?? trimmed.replace(/\s+/g, "");
|
|
}
|
|
|
|
/**
|
|
* @param {string} group
|
|
* @returns {string}
|
|
*/
|
|
export function getTypedGroupDisplayName(group) {
|
|
const key = normalizeTypedGroup(group);
|
|
const i18nKey = TYPED_GROUP_I18N[key];
|
|
if (i18nKey) return game.i18n.localize(i18nKey);
|
|
return group;
|
|
}
|
|
|
|
/**
|
|
* @param {ProfessionSkillRef} ref
|
|
* @returns {string}
|
|
*/
|
|
export function formatProfessionSkillKey(ref) {
|
|
if (ref.kind === "fixed") return ref.key;
|
|
const groupLabel = getTypedGroupDisplayName(ref.group);
|
|
return `${groupLabel} (${ref.label.trim()})`;
|
|
}
|
|
|
|
/**
|
|
* Storage key for a profession skill map entry.
|
|
*
|
|
* @param {ProfessionSkillRef} ref
|
|
* @param {{ chooseOne?: boolean }} [options]
|
|
* @returns {string}
|
|
*/
|
|
export function allocateProfessionSkillStorageKey(
|
|
ref,
|
|
{ chooseOne = false } = {},
|
|
) {
|
|
if (ref.kind === "fixed") return ref.key;
|
|
if (chooseOne) {
|
|
return `typed:${normalizeTypedGroup(ref.group)}:${foundry.utils.randomID(
|
|
8,
|
|
)}`;
|
|
}
|
|
const label = ref.label?.trim() ?? "";
|
|
return formatProfessionSkillKey({
|
|
kind: "typed",
|
|
group: ref.group,
|
|
label,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @returns {ProfessionSkillRef | null}
|
|
*/
|
|
export function parseProfessionSkillKey(key) {
|
|
if (!key || key === PROFESSION_OPTION_PICKS_KEY) return null;
|
|
if (DG.skills.includes(key)) return { kind: "fixed", key };
|
|
|
|
const internalMatch = key.match(INTERNAL_TYPED_KEY_PATTERN);
|
|
if (internalMatch) {
|
|
let label = "";
|
|
if (internalMatch[3] !== undefined) {
|
|
try {
|
|
label = decodeURIComponent(internalMatch[3]);
|
|
} catch {
|
|
label = internalMatch[3];
|
|
}
|
|
}
|
|
return {
|
|
kind: "typed",
|
|
group: internalMatch[1],
|
|
label,
|
|
};
|
|
}
|
|
|
|
const match = key.match(TYPED_KEY_PATTERN);
|
|
if (match) {
|
|
return {
|
|
kind: "typed",
|
|
group: normalizeTypedGroup(match[1]),
|
|
label: match[2].trim(),
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {ProfessionSkillRef} ref
|
|
* @returns {string}
|
|
*/
|
|
export function formatProfessionSkillLabel(ref) {
|
|
if (ref.kind === "fixed") {
|
|
if (game.i18n.lang === "ja") {
|
|
const ja = game.i18n.localize(`DG.Skills.ruby.${ref.key}`);
|
|
if (ja && ja !== `DG.Skills.ruby.${ref.key}`) return ja;
|
|
}
|
|
const label = game.i18n.localize(`DG.Skills.${ref.key}`);
|
|
if (label && label !== `DG.Skills.${ref.key}`) return label;
|
|
return ref.key;
|
|
}
|
|
if (!ref.label?.trim()) return getTypedGroupDisplayName(ref.group);
|
|
return formatProfessionSkillKey(ref);
|
|
}
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @param {object} [metaContext]
|
|
* @param {Record<string, ProfessionSkillMeta>} [metaContext.automaticMeta]
|
|
* @param {Record<string, ProfessionSkillMeta>} [metaContext.optionMeta]
|
|
* @returns {string}
|
|
*/
|
|
export function formatProfessionSkillKeyLabel(
|
|
key,
|
|
{ automaticMeta = {}, optionMeta = {} } = {},
|
|
) {
|
|
const ref = parseProfessionSkillKey(key);
|
|
if (!ref) return key;
|
|
if (
|
|
ref.kind === "typed" &&
|
|
isChooseOneProfessionSkillKey(key, automaticMeta, optionMeta)
|
|
) {
|
|
return formatProfessionSkillKey({
|
|
kind: "typed",
|
|
group: ref.group,
|
|
label: getChooseOnePlaceholderLabel(),
|
|
});
|
|
}
|
|
if (ref.kind === "typed" && !ref.label?.trim()) {
|
|
return getTypedGroupDisplayName(ref.group);
|
|
}
|
|
return formatProfessionSkillLabel(ref);
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, number>} obj
|
|
* @returns {{ optionPicks: number, skills: Record<string, number> }}
|
|
*/
|
|
export function splitProfessionSkillMap(obj) {
|
|
const source = obj ?? {};
|
|
const optionPicks = Number(source[PROFESSION_OPTION_PICKS_KEY]) || 0;
|
|
/** @type {Record<string, number>} */
|
|
const skills = {};
|
|
for (const [key, value] of Object.entries(source)) {
|
|
if (key === PROFESSION_OPTION_PICKS_KEY) continue;
|
|
skills[key] = Number(value);
|
|
}
|
|
return { optionPicks, skills };
|
|
}
|