import { BONUS_SKILL_COUNT, BONUS_SKILL_INCREMENT, SKILL_CAP, } from "./constants.js"; import { catalogIdToSkillRef, collectBonusCapValidationErrors, } from "./catalog.js"; import { formatProfessionSkillKey, formatProfessionSkillLabel, getAgentSkillDefaults, parseProfessionSkillKey, } from "./keys.js"; import { validateProfessionFormState } from "./validation.js"; /** * @param {Record} automaticSkills * @param {Record} optionSkills * @param {number} optionPicks * @param {object} formState * @param {Set} formState.checkedOptionKeys * @param {Record} formState.chooseOneLabels - keyed by profession map key * @param {string[]} formState.bonusCatalogIds - length 8 * @param {string[]} formState.bonusTypedLabels - length 8 * @param {string[]} [formState.bondNames] * @param {string[]} [formState.bondRelationships] * @param {object} [context] * @param {Record} [context.automaticMeta] * @param {Record} [context.optionMeta] * @param {number} [context.bondCount] * @returns {{ * fixedValues: Record, * typedValues: Record, * capWarnings: { label: string, attempted: number, waste: number }[], * isValid: boolean, * validationErrors: string[], * modifiedFixedKeys: string[], * modifiedTypedKeys: string[], * }} */ export default function computeSkillValues( automaticSkills, optionSkills, optionPicks, formState, { automaticMeta = {}, optionMeta = {}, bondCount = 0 } = {}, ) { const defaults = getAgentSkillDefaults(); /** @type {Record} */ const fixedValues = { ...defaults }; /** @type {Record} */ const typedValues = {}; /** @type {Set} */ const modifiedFixedKeys = new Set(); /** @type {Set} */ const modifiedTypedKeys = new Set(); /** * @param {TypedSkillRef} ref * @param {string} label */ const markTypedModified = (ref, label) => { modifiedTypedKeys.add( formatProfessionSkillKey({ kind: "typed", group: ref.group, label }), ); }; /** * @param {string} mapKey * @param {number} rating * @param {{ labelOverride?: string }} [options] * @returns {string | null} Merged typed storage key, if applied. */ const applyOverride = (mapKey, rating, options = {}) => { const ref = parseProfessionSkillKey(mapKey); if (!ref) return null; const value = Number(rating); if (ref.kind === "fixed") { fixedValues[ref.key] = value; return null; } const label = options.labelOverride?.trim() ?? ref.label?.trim() ?? ""; if (!label) return null; const storageKey = formatProfessionSkillKey({ kind: "typed", group: ref.group, label, }); typedValues[storageKey] = { group: ref.group, label, value, }; return storageKey; }; for (const [key, rating] of Object.entries(automaticSkills ?? {})) { const ref = parseProfessionSkillKey(key); if (ref) { let labelOverride; let skip = false; if (ref.kind === "typed" && automaticMeta[key]?.chooseOne) { labelOverride = formState.chooseOneLabels?.[key]?.trim(); if (!labelOverride) skip = true; } if (!skip) { const storageKey = applyOverride(key, rating, { labelOverride }); if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key); else if (storageKey) { markTypedModified(ref, labelOverride ?? ref.label); } } } } for (const key of formState.checkedOptionKeys ?? []) { if (optionSkills[key] || optionSkills[key] === 0) { const ref = parseProfessionSkillKey(key); if (ref) { let labelOverride; let skip = false; if (ref.kind === "typed" && optionMeta[key]?.chooseOne) { labelOverride = formState.chooseOneLabels?.[key]?.trim(); if (!labelOverride) skip = true; } else if (ref.kind === "typed") { labelOverride = ref.label?.trim(); if (!labelOverride) skip = true; } if (!skip) { const storageKey = applyOverride(key, optionSkills[key], { labelOverride, }); if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key); else if (storageKey) { markTypedModified(ref, labelOverride ?? ref.label); } } } } } /** @type {Record} */ const bonusCounts = {}; const bonusIds = formState.bonusCatalogIds ?? []; const bonusLabels = formState.bonusTypedLabels ?? []; for (let i = 0; i < BONUS_SKILL_COUNT; i++) { const catalogId = bonusIds[i]; if (catalogId) { const typedLabel = bonusLabels[i] ?? ""; const ref = catalogIdToSkillRef(catalogId, typedLabel); if (ref) { if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key); else modifiedTypedKeys.add(formatProfessionSkillKey(ref)); let trackKey; if (ref.kind === "fixed") { trackKey = `fixed:${ref.key}`; } else { trackKey = formatProfessionSkillKey(ref); } bonusCounts[trackKey] = (bonusCounts[trackKey] ?? 0) + 1; } } } /** Pre-bonus base for cap warnings */ const baseFixed = { ...fixedValues }; const baseTyped = foundry.utils.deepClone(typedValues); for (const [trackKey, count] of Object.entries(bonusCounts)) { const bonus = count * BONUS_SKILL_INCREMENT; if (trackKey.startsWith("fixed:")) { const key = trackKey.slice(6); fixedValues[key] = (fixedValues[key] ?? defaults[key] ?? 0) + bonus; } else { const ref = parseProfessionSkillKey(trackKey); if (ref?.kind === "typed") { const base = typedValues[trackKey]?.value ?? 0; typedValues[trackKey] = { group: ref.group, label: ref.label, value: base + bonus, }; } } } /** @type {{ label: string, attempted: number, waste: number }[]} */ const capWarnings = []; const checkCap = (label, base, final) => { if (final > SKILL_CAP) { capWarnings.push({ label, attempted: final, waste: final - SKILL_CAP, }); } }; for (const [key, final] of Object.entries(fixedValues)) { const base = baseFixed[key] ?? defaults[key] ?? 0; const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key }); checkCap(formatProfessionSkillLabel(ref), base, final); } for (const [storageKey, data] of Object.entries(typedValues)) { const base = baseTyped[storageKey]?.value ?? 0; const ref = /** @type {TypedSkillRef} */ ({ kind: "typed", group: data.group, label: data.label, }); checkCap(formatProfessionSkillLabel(ref), base, data.value); } /** Apply cap for display (values shown capped at 80) */ for (const key of Object.keys(fixedValues)) { if (fixedValues[key] > SKILL_CAP) fixedValues[key] = SKILL_CAP; } for (const data of Object.values(typedValues)) { if (data.value > SKILL_CAP) data.value = SKILL_CAP; } const validationErrors = validateProfessionFormState(optionPicks, formState, { automaticSkills, optionSkills, automaticMeta, optionMeta, bondCount, }); validationErrors.push( ...collectBonusCapValidationErrors( baseFixed, baseTyped, bonusIds, bonusLabels, ), ); return { fixedValues, typedValues, capWarnings, isValid: validationErrors.length === 0, validationErrors, modifiedFixedKeys: [...modifiedFixedKeys], modifiedTypedKeys: [...modifiedTypedKeys], }; }