import { BONUS_SKILL_COUNT } from "./constants.js"; import { catalogIdToSkillRef, getBonusTrackLabel } from "./catalog.js"; import { getTypedGroupDisplayName, normalizeTypedGroup, normalizeTypedSkillName, parseProfessionSkillKey, } from "./keys.js"; /** * @param {number} optionPicks * @param {object} formState * @param {object} [context] * @param {Record} [context.automaticSkills] * @param {Record} [context.optionSkills] * @param {Record} [context.automaticMeta] * @param {Record} [context.optionMeta] * @param {number} [context.bondCount] * @returns {string[]} */ export function validateProfessionFormState( optionPicks, formState, context = {}, ) { const errors = []; const checked = formState.checkedOptionKeys ?? new Set(); const picks = Number(optionPicks) || 0; const { automaticSkills = {}, automaticMeta = {}, optionMeta = {}, bondCount = 0, } = context; if (checked.size !== picks) { errors.push("optionPicks"); } for (let i = 0; i < bondCount; i++) { if (!formState.bondNames?.[i]?.trim()) errors.push(`bondName${i}`); if (!formState.bondRelationships?.[i]?.trim()) { errors.push(`bondRelationship${i}`); } } for (let i = 0; i < BONUS_SKILL_COUNT; i++) { const catalogId = formState.bonusCatalogIds?.[i]; if (!catalogId) { errors.push(`bonus${i}`); } else { const ref = catalogIdToSkillRef( catalogId, formState.bonusTypedLabels?.[i] ?? "", ); if (!ref) { errors.push(`bonus${i}`); } else if ( ref.kind === "typed" && !formState.bonusTypedLabels?.[i]?.trim() ) { errors.push(`bonusType${i}`); } } } /** @type {Map>} */ const resolvedByGroup = new Map(); const registerResolvedName = (ref, rawLabel) => { const label = rawLabel?.trim(); if (!label) { errors.push(`typedNameRequired:${ref.group}`); return; } const group = normalizeTypedGroup(ref.group); const normalized = normalizeTypedSkillName(label); if (!resolvedByGroup.has(group)) resolvedByGroup.set(group, new Set()); const names = resolvedByGroup.get(group); if (names.has(normalized)) { errors.push(`typedNameConflict:${group}:${label}`); return; } names.add(normalized); }; for (const [key] of Object.entries(automaticSkills)) { const ref = parseProfessionSkillKey(key); if (ref && ref.kind === "typed") { if (automaticMeta[key]?.chooseOne) { registerResolvedName(ref, formState.chooseOneLabels?.[key]); } else { registerResolvedName(ref, ref.label); } } } for (const key of checked) { const ref = parseProfessionSkillKey(key); if (ref && ref.kind === "typed") { if (optionMeta[key]?.chooseOne) { registerResolvedName(ref, formState.chooseOneLabels?.[key]); } else { registerResolvedName(ref, ref.label); } } } return errors; } /** * User-facing messages for Add Profession validation error codes. * @param {string[]} errors * @param {object} [context] * @param {number} [context.optionPicks] * @returns {string[]} */ export function formatProfessionValidationMessages( errors, { optionPicks = 0, bonusCount = BONUS_SKILL_COUNT, skillCap = 80 } = {}, ) { const messages = []; const seen = new Set(); const push = (msg) => { if (seen.has(msg)) return; seen.add(msg); messages.push(msg); }; let needsOptionPicks = false; let needsBondName = false; let needsBondRelationship = false; let needsBonusSkill = false; let needsBonusSkillType = false; /** @type {Set} */ const typedNameRequiredGroups = new Set(); /** @type {Set} */ const bonusWasteTrackKeys = new Set(); for (const code of errors) { if (code === "optionPicks") { needsOptionPicks = true; } else if (code.startsWith("bondName")) { needsBondName = true; } else if (code.startsWith("bondRelationship")) { needsBondRelationship = true; } else { const bonusWaste = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/); if (bonusWaste) { bonusWasteTrackKeys.add(bonusWaste[2]); } else if (code.startsWith("bonusType")) { needsBonusSkillType = true; } else if (/^bonus\d+$/.test(code)) { needsBonusSkill = true; } else { const typedNameRequired = code.match(/^typedNameRequired:(.+)$/); if (typedNameRequired) { typedNameRequiredGroups.add(typedNameRequired[1]); } else { const conflict = code.match(/^typedNameConflict:([^:]+):(.+)$/); if (conflict) { const [, group, name] = conflict; push( game.i18n.format("DG.Profession.Dialog.DuplicateTypedName", { type: getTypedGroupDisplayName(group), name, }), ); } } } } } if (needsOptionPicks) { push( game.i18n.format("DG.Profession.Dialog.OptionPicksRequired", { picks: optionPicks, }), ); } if (needsBonusSkill) { push( game.i18n.format("DG.Profession.Dialog.BonusSkillRequired", { count: bonusCount, }), ); } if (needsBonusSkillType) { push(game.i18n.localize("DG.Profession.Dialog.BonusSkillTypeRequired")); } for (const group of typedNameRequiredGroups) { push( game.i18n.format("DG.Profession.Dialog.TypedNameRequired", { type: getTypedGroupDisplayName(group), }), ); } if (needsBondName) { push(game.i18n.localize("DG.Profession.Dialog.BondNameRequired")); } if (needsBondRelationship) { push(game.i18n.localize("DG.Profession.Dialog.BondRelationshipRequired")); } for (const trackKey of bonusWasteTrackKeys) { push( game.i18n.format("DG.Profession.Dialog.BonusSkillWasteTooHigh", { skill: getBonusTrackLabel(trackKey), }), ); } return messages; }