Files
DeltaGreen/module/profession/validation.js
T

218 lines
6.0 KiB
JavaScript

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<string, number>} [context.automaticSkills]
* @param {Record<string, number>} [context.optionSkills]
* @param {Record<string, ProfessionSkillMeta>} [context.automaticMeta]
* @param {Record<string, ProfessionSkillMeta>} [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<string, Set<string>>} */
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<string>} */
const typedNameRequiredGroups = new Set();
/** @type {Set<string>} */
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;
}