mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2025-10-28 20:45:35 -05:00
v1.198.1
This commit is contained in:
757
js/converterutils-background.js
Normal file
757
js/converterutils-background.js
Normal file
@@ -0,0 +1,757 @@
|
||||
"use strict";
|
||||
|
||||
class BackgroundConverterConst {
|
||||
static RE_NAME_SKILLS = /^Skill Proficienc(?:ies|y):/;
|
||||
static RE_NAME_TOOLS = /^(?:Tools?|Tool Proficienc(?:ies|y)):/;
|
||||
static RE_NAME_LANGUAGES = /^Languages?:/;
|
||||
static RE_NAME_EQUIPMENT = /^Equipment?:/;
|
||||
}
|
||||
|
||||
class UtilBackgroundParser {
|
||||
static getEquipmentEntry (background) {
|
||||
const list = background.entries.find(ent => ent.type === "list");
|
||||
if (!list) return null;
|
||||
return list.items.find(ent => BackgroundConverterConst.RE_NAME_EQUIPMENT.test(ent.name));
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.UtilBackgroundParser = UtilBackgroundParser;
|
||||
|
||||
class EquipmentBreakdown {
|
||||
static _WALKER;
|
||||
|
||||
static tryRun (
|
||||
bg,
|
||||
{
|
||||
isSkipExisting = false,
|
||||
cbWarning = () => {},
|
||||
mappingsManual = {},
|
||||
allowlistOrEnds = [],
|
||||
blocklistSplits = [],
|
||||
} = {},
|
||||
) {
|
||||
if (!bg.entries) return cbWarning(`No entries found on "${bg.name}"`);
|
||||
|
||||
if (isSkipExisting && bg.startingEquipment) return;
|
||||
delete bg.startingEquipment;
|
||||
|
||||
this._WALKER ||= MiscUtil.getWalker({isNoModification: true, isBreakOnReturn: true});
|
||||
|
||||
const entryEquipment = UtilBackgroundParser.getEquipmentEntry(bg);
|
||||
if (!entryEquipment.entry) throw new Error(`Unimplemented!`);
|
||||
|
||||
if (!entryEquipment) return;
|
||||
this._convert({
|
||||
bg,
|
||||
entry: entryEquipment.entry,
|
||||
mappingsManual,
|
||||
allowlistOrEnds,
|
||||
blocklistSplits,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Output structure:
|
||||
*
|
||||
* ```
|
||||
* equipment: [
|
||||
* {
|
||||
* "_": [
|
||||
* <equipment details>
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* "a": [
|
||||
* <equipment details; choice A>
|
||||
* ],
|
||||
* "b": [
|
||||
* <equipment details; choice B>
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param bg
|
||||
* @param entry
|
||||
* @param mappingsManual
|
||||
* @param allowlistOrEnds
|
||||
* @param blocklistSplits
|
||||
* @private
|
||||
*/
|
||||
static _convert (
|
||||
{
|
||||
bg,
|
||||
entry,
|
||||
mappingsManual,
|
||||
allowlistOrEnds,
|
||||
blocklistSplits,
|
||||
},
|
||||
) {
|
||||
blocklistSplits.forEach((str, i) => entry = entry.replace(str, `__SPLIT_${i}__`));
|
||||
const parts = entry
|
||||
.split(/\. /g)
|
||||
.map(it => it.trim())
|
||||
.map(it => it.split(StrUtil.COMMAS_NOT_IN_PARENTHESES_REGEX).map(it => it.trim()))
|
||||
.flat()
|
||||
.map(it => {
|
||||
return it.replace(/__SPLIT_(\d+)__/gi, (...m) => {
|
||||
const ix = Number(m[1]);
|
||||
return blocklistSplits[ix];
|
||||
});
|
||||
});
|
||||
|
||||
const out = parts
|
||||
.map(pt => {
|
||||
// Strip leading "and" and trailing punctuation
|
||||
pt = pt
|
||||
.trim()
|
||||
.replace(/^and /i, "")
|
||||
.replace(/[.!?]$/, "")
|
||||
.trim();
|
||||
|
||||
// Split up choices
|
||||
const ptChoices = this._splitChoices({str: pt, allowlistOrEnds})
|
||||
.map(c => c.trim().replace(/,$/, "").trim());
|
||||
|
||||
const outChoices = ptChoices
|
||||
.map(ch => {
|
||||
const chOriginal = ch;
|
||||
|
||||
// Pull out quantities
|
||||
let quantity = 1;
|
||||
ch = ch
|
||||
.replace(/^(any one|an|a|one|two|three|four|five|six|seven|eight|nine|ten|\d+)/i, (...m) => {
|
||||
quantity = Parser.textToNumber(
|
||||
m[1]
|
||||
.replace(/^any/i, "").trim(),
|
||||
);
|
||||
return "";
|
||||
})
|
||||
.trim();
|
||||
|
||||
if (isNaN(quantity)) throw new Error(`Quantity found in "${chOriginal}" was not a number!`);
|
||||
|
||||
// Pull out coinage
|
||||
let valueCp = 0;
|
||||
let cntValueContainingWith = 0;
|
||||
let cntValueWorth = 0;
|
||||
ch = ch
|
||||
// Remove trailing parenthetical parts, e.g. "... (Azorius-minted 1-zino coins)"
|
||||
.replace(/(containing|with|worth) (\d+\s*[csgep]p)(\s+\([^)]+\))?/g, (...m) => {
|
||||
switch (m[1].toLowerCase().trim()) {
|
||||
case "containing":
|
||||
case "with": cntValueContainingWith += 1; break;
|
||||
case "worth": cntValueWorth += 1; break;
|
||||
default: throw new Error(`Unhandled "${m[1]}"`);
|
||||
}
|
||||
|
||||
valueCp += Parser.coinValueToNumber(m[2]);
|
||||
return "";
|
||||
})
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
// Handle e.g. "1 sp"--the quantity will have been pulled out already
|
||||
.replace(/^[csgep]p$/g, (...m) => {
|
||||
valueCp = Parser.coinValueToNumber(`${quantity} ${m[0]}`);
|
||||
return "";
|
||||
})
|
||||
.trim()
|
||||
// Handle e.g. "(10 gp)"
|
||||
.replace(/\((\d+\s*[csgep]p)\)/g, (...m) => {
|
||||
valueCp += Parser.coinValueToNumber(m[1]);
|
||||
return "";
|
||||
})
|
||||
.trim();
|
||||
|
||||
// Pull out "set of... " for clothes
|
||||
if (/clothes/i.test(ch)) {
|
||||
ch = ch.replace(/^set of /i, "");
|
||||
}
|
||||
|
||||
// Pull out parenthetical parts that aren't in @item tags
|
||||
const ptParenMeta = this._getWithoutParenParts(ch);
|
||||
let ptDisplay = ch; // Keep a copy of the part to display as the item name
|
||||
let ptDisplayNoTags = Renderer.stripTags(ptDisplay);
|
||||
|
||||
let ptPlain = ptParenMeta.plain.map(it => it.trim()).join(" ").trim();
|
||||
let ptParens = ptParenMeta.inParens.map(it => it.trim()).join(" ").trim();
|
||||
|
||||
// Handle any manual mappings
|
||||
if (mappingsManual[chOriginal]) {
|
||||
return mappingsManual[chOriginal];
|
||||
}
|
||||
|
||||
// Handle any pure coinage
|
||||
if (!ptPlain && valueCp) {
|
||||
return {value: valueCp};
|
||||
}
|
||||
|
||||
// If the plain part seems to just be a single @item tag, use this
|
||||
const mItem = /{@item ([^}]+)}/.exec(ptPlain);
|
||||
const mFilter = /{@filter ([^}]+)}/.exec(ptPlain);
|
||||
|
||||
// If the plain part is a flavorful name of an @item, use this
|
||||
const mItemParens = /^\({@item ([^}]+)}\)$/.exec(ptParens);
|
||||
|
||||
if (mItem) {
|
||||
// consider doing something with displayText?
|
||||
const [name, source, displayText] = mItem[1].split("|").map(it => it.trim()).filter(Boolean);
|
||||
const idItem = [name, source].join("|").toLowerCase();
|
||||
|
||||
if (quantity !== 1 || ptPlain !== ptDisplay || valueCp) {
|
||||
const info = {item: idItem};
|
||||
|
||||
if (quantity !== 1) info.quantity = quantity;
|
||||
if (ptPlain !== ptDisplay) info.displayName = ptDisplayNoTags;
|
||||
if (valueCp) info.containsValue = valueCp;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
if (!ptPlain.startsWith("{@") || !ptPlain.endsWith("}")) {
|
||||
return {
|
||||
item: idItem,
|
||||
displayName: ptDisplayNoTags,
|
||||
};
|
||||
}
|
||||
|
||||
return idItem;
|
||||
}
|
||||
|
||||
if (mFilter) {
|
||||
// Strip junk text
|
||||
let ptPlainFilter = ptPlain
|
||||
.replace(/^set of/gi, "").trim()
|
||||
.replace(/ you are proficient with$/gi, "").trim()
|
||||
.replace(/ with which you are proficient$/gi, "").trim()
|
||||
.replace(/ of your choice$/gi, "").trim()
|
||||
.replace(/ \([^)]+\)/gi, "").trim()
|
||||
// Brew
|
||||
.replace(/^wind /gi, "").trim();
|
||||
|
||||
// We expect that the entire text is now a filter tag
|
||||
if (!ptPlainFilter.startsWith("{@") || !ptPlainFilter.endsWith("}")) throw new Error(`Text "${ptPlainFilter}" was not a single tag!`);
|
||||
|
||||
const info = this._getFilterType(mFilter[1].split("|")[0]);
|
||||
if (quantity !== 1) info.quantity = quantity;
|
||||
if (valueCp) info.containsValue = valueCp;
|
||||
return info;
|
||||
}
|
||||
|
||||
if (mItemParens) {
|
||||
// consider doing something with displayText?
|
||||
const [name, source, displayText] = mItemParens[1].split("|").map(it => it.trim()).filter(Boolean);
|
||||
const idItem = [name, source].join("|").toLowerCase();
|
||||
|
||||
const info = {
|
||||
item: idItem,
|
||||
displayName: ptPlain,
|
||||
};
|
||||
|
||||
if (quantity !== 1) info.quantity = quantity;
|
||||
if (valueCp) info.containsValue = valueCp;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// Otherwise, create a custom item
|
||||
const info = {special: ptDisplayNoTags.trim()};
|
||||
if (quantity !== 1) info.quantity = quantity;
|
||||
if (valueCp) {
|
||||
if (cntValueWorth > cntValueContainingWith) info.worthValue = valueCp;
|
||||
else info.containsValue = valueCp;
|
||||
}
|
||||
return info;
|
||||
})
|
||||
.flat();
|
||||
|
||||
// Assign each choice a letter (or use underscore if it's the only choice)
|
||||
if (outChoices.length === 1) {
|
||||
if (outChoices[0].isList) return {"_": outChoices[0].data};
|
||||
return {"_": [outChoices[0]]};
|
||||
}
|
||||
|
||||
const outPart = {};
|
||||
outChoices.forEach((ch, i) => {
|
||||
const letter = Parser.ALPHABET[i].toLowerCase();
|
||||
outPart[letter] = [ch];
|
||||
});
|
||||
return outPart;
|
||||
});
|
||||
|
||||
// Combine no-choice sections together
|
||||
const outReduced = [];
|
||||
out
|
||||
.forEach(info => {
|
||||
if (!info._) return outReduced.push(info);
|
||||
|
||||
const existing = outReduced.find(x => x._);
|
||||
if (existing) return existing._.push(...info._);
|
||||
return outReduced.push(info);
|
||||
});
|
||||
|
||||
bg.startingEquipment = outReduced;
|
||||
}
|
||||
|
||||
static _splitChoices ({str, allowlistOrEnds}) {
|
||||
const out = [];
|
||||
|
||||
let expectAt = false;
|
||||
let braceDepth = 0;
|
||||
let parenDepth = 0;
|
||||
let stack = "";
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
const c = str[i];
|
||||
switch (c) {
|
||||
case "(": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
parenDepth++;
|
||||
|
||||
break;
|
||||
}
|
||||
case ")": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
if (parenDepth) parenDepth--;
|
||||
|
||||
break;
|
||||
}
|
||||
case "{": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
braceDepth++;
|
||||
expectAt = true;
|
||||
|
||||
break;
|
||||
}
|
||||
case "}": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
if (braceDepth) braceDepth--;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "@": { expectAt = false; stack += c; break; }
|
||||
|
||||
case " ": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
if (
|
||||
!braceDepth
|
||||
&& !parenDepth
|
||||
) {
|
||||
// An oxford comma implies earlier commas in this part are separating or'd parts, so back-split
|
||||
if (
|
||||
stack.endsWith(", or ")
|
||||
&& !allowlistOrEnds.some(it => stack.endsWith(it))
|
||||
) {
|
||||
const backSplit = stack.slice(0, -5).split(StrUtil.COMMAS_NOT_IN_PARENTHESES_REGEX).map(it => it.trim());
|
||||
out.push(...backSplit);
|
||||
stack = "";
|
||||
} else if (
|
||||
stack.endsWith(" or ")
|
||||
&& !allowlistOrEnds.some(it => stack.endsWith(it))
|
||||
) {
|
||||
out.push(stack.slice(0, -4));
|
||||
stack = "";
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
stack += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push(stack);
|
||||
|
||||
// Split two conjoined items
|
||||
if (out.length === 1 && out[0].includes(" and ")) {
|
||||
let cntItem = 0;
|
||||
out[0].replace(/{@item/g, () => {
|
||||
cntItem++;
|
||||
return "";
|
||||
});
|
||||
if (cntItem > 1) {
|
||||
if (cntItem !== 2) throw new Error(`Unhandled conjunction count "${cntItem}"`);
|
||||
|
||||
const spl = out[0].split(" and ");
|
||||
out[0] = spl[0];
|
||||
out.push(spl[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static _getWithoutParenParts (str) {
|
||||
const out = [];
|
||||
const outParens = [];
|
||||
|
||||
let expectAt = false;
|
||||
let braceDepth = 0;
|
||||
let parenCount = 0;
|
||||
let stack = "";
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
const c = str[i];
|
||||
switch (c) {
|
||||
case "(": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
if (!braceDepth) {
|
||||
if (!parenCount) {
|
||||
out.push(stack);
|
||||
stack = "";
|
||||
}
|
||||
|
||||
parenCount++;
|
||||
}
|
||||
|
||||
stack += c;
|
||||
|
||||
break;
|
||||
}
|
||||
case ")": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
|
||||
if (!braceDepth && parenCount) {
|
||||
parenCount--;
|
||||
|
||||
if (!parenCount) {
|
||||
outParens.push(stack);
|
||||
stack = "";
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "{": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
braceDepth++;
|
||||
expectAt = true;
|
||||
|
||||
stack += c;
|
||||
|
||||
break;
|
||||
}
|
||||
case "}": {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
|
||||
stack += c;
|
||||
if (braceDepth) braceDepth--;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "@": { expectAt = false; stack += c; break; }
|
||||
|
||||
default: {
|
||||
if (expectAt) { braceDepth--; expectAt = false; }
|
||||
stack += c; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gather any leftovers
|
||||
if (!braceDepth) {
|
||||
if (!parenCount) out.push(stack);
|
||||
else outParens.push(stack);
|
||||
} else {
|
||||
out.push(stack);
|
||||
}
|
||||
|
||||
return {plain: out.filter(Boolean), inParens: outParens.filter(Boolean)};
|
||||
}
|
||||
|
||||
static _getFilterType (str) {
|
||||
switch (str.toLowerCase().trim()) {
|
||||
case "artisan's tools": return {equipmentType: "toolArtisan"};
|
||||
case "gaming set": return {equipmentType: "setGaming"};
|
||||
case "musical instrument": return {equipmentType: "instrumentMusical"};
|
||||
|
||||
default: throw new Error(`Unhandled filter type "${str}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.EquipmentBreakdown = EquipmentBreakdown;
|
||||
|
||||
class BackgroundSkillTollLanguageEquipmentCoalesce {
|
||||
static tryRun (
|
||||
bg,
|
||||
{
|
||||
cbWarning = () => {},
|
||||
} = {},
|
||||
) {
|
||||
if (!bg.entries) return;
|
||||
|
||||
const [entriesToCompact, entriesOther] = bg.entries.segregate(ent => this._isToCompact(ent));
|
||||
|
||||
if (!entriesToCompact.length) return;
|
||||
|
||||
const list = {
|
||||
"type": "list",
|
||||
"style": "list-hang-notitle",
|
||||
};
|
||||
list.items = entriesToCompact
|
||||
.map(ent => {
|
||||
return ent.entries.length === 1
|
||||
? {
|
||||
type: "item",
|
||||
name: ent.name,
|
||||
entry: ent.entries[0],
|
||||
}
|
||||
: {
|
||||
type: "item",
|
||||
name: ent.name,
|
||||
entries: ent.entries,
|
||||
};
|
||||
});
|
||||
|
||||
bg.entries = [
|
||||
list,
|
||||
...entriesOther,
|
||||
];
|
||||
}
|
||||
|
||||
static _RES_COMPACT = [
|
||||
BackgroundConverterConst.RE_NAME_SKILLS,
|
||||
BackgroundConverterConst.RE_NAME_TOOLS,
|
||||
BackgroundConverterConst.RE_NAME_LANGUAGES,
|
||||
/^Equipment:/,
|
||||
];
|
||||
|
||||
static _isToCompact (ent) {
|
||||
return this._RES_COMPACT
|
||||
.some(re => re.test(ent.name));
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.BackgroundSkillTollLanguageEquipmentCoalesce = BackgroundSkillTollLanguageEquipmentCoalesce;
|
||||
|
||||
class BackgroundSkillToolLanguageTag {
|
||||
static tryRun (
|
||||
bg,
|
||||
{
|
||||
cbWarning = () => {},
|
||||
} = {},
|
||||
) {
|
||||
const list = bg.entries.find(ent => ent.type === "list");
|
||||
|
||||
this._doSkillTag({bg, list, cbWarning});
|
||||
this._doToolTag({bg, list, cbWarning});
|
||||
this._doLanguageTag({bg, list, cbWarning});
|
||||
}
|
||||
|
||||
static _doSkillTag ({bg, list, cbWarning}) {
|
||||
const skillProf = list.items.find(ent => BackgroundConverterConst.RE_NAME_SKILLS.test(ent.name));
|
||||
if (!skillProf) return;
|
||||
|
||||
const mOneStaticOneChoice = /^(?<predefined>.*)\band one choice from the following:(?<choices>.*)$/i.exec(skillProf.entry);
|
||||
if (mOneStaticOneChoice) {
|
||||
const predefined = {};
|
||||
mOneStaticOneChoice.groups.predefined
|
||||
.replace(/{@skill (?<skill>[^}]+)/g, (...m) => {
|
||||
predefined[m.last().skill.toLowerCase().trim()] = true;
|
||||
return "";
|
||||
});
|
||||
|
||||
if (!Object.keys(predefined).length) return cbWarning(`(${bg.name}) Skills require manual tagging`);
|
||||
|
||||
const choices = [];
|
||||
mOneStaticOneChoice.groups.choices
|
||||
.replace(/{@skill (?<skill>[^}]+)/g, (...m) => {
|
||||
choices.push(m.last().skill.toLowerCase().trim());
|
||||
return "";
|
||||
});
|
||||
|
||||
bg.skillProficiencies = [
|
||||
{
|
||||
...predefined,
|
||||
choose: {
|
||||
from: choices,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^Two of the following:/.test(skillProf.entry)) {
|
||||
const choices = [];
|
||||
skillProf.entry
|
||||
.replace(/{@skill (?<skill>[^}]+)/g, (...m) => {
|
||||
choices.push(m.last().skill.toLowerCase().trim());
|
||||
return "";
|
||||
});
|
||||
|
||||
bg.skillProficiencies = [
|
||||
{
|
||||
choose: {
|
||||
from: choices,
|
||||
count: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^({@skill [^}]+}(?:, )?)+$/.test(skillProf.entry)) return cbWarning(`(${bg.name}) Skills require manual tagging`);
|
||||
|
||||
bg.skillProficiencies = [
|
||||
skillProf.entry
|
||||
.split(",")
|
||||
.map(ent => ent.trim())
|
||||
.mergeMap(str => {
|
||||
const reTag = /^{@skill (?<skill>[^}]+)}$/.exec(str);
|
||||
if (reTag) return {[reTag.groups.skill.toLowerCase().trim()]: true};
|
||||
throw new Error(`Couldn't find tag in ${str}`);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
static _doToolTag ({bg, list, cbWarning}) {
|
||||
const toolProf = list.items.find(ent => BackgroundConverterConst.RE_NAME_TOOLS.test(ent.name));
|
||||
if (!toolProf) return;
|
||||
|
||||
const entry = Renderer.stripTags(toolProf.entry.toLowerCase())
|
||||
.replace(/one type of gaming set/g, "gaming set")
|
||||
.replace(/one type of artisan's tools/g, "artisan's tools")
|
||||
.replace(/one type of gaming set/g, "gaming set")
|
||||
.replace(/one type of musical instrument/g, "musical instrument")
|
||||
.replace(/one other set of artisan's tools/g, "artisan's tools")
|
||||
.replace(/s' supplies/g, "'s supplies")
|
||||
;
|
||||
|
||||
const isChoice = /\bany |\bchoose |\bone type|\bchoice|\bor /g.exec(entry);
|
||||
const isSpecial = /\bspecial/.exec(entry);
|
||||
|
||||
if (!isChoice && !isSpecial) {
|
||||
bg.toolProficiencies = [
|
||||
entry.toLowerCase()
|
||||
.split(/,\s?(?![^(]*\))| or | and /g)
|
||||
.filter(Boolean)
|
||||
.map(pt => pt.trim())
|
||||
.filter(pt => pt)
|
||||
.mergeMap(pt => ({[pt]: true})),
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
if (isChoice) {
|
||||
const entryClean = entry
|
||||
.replace(/^either /gi, "");
|
||||
|
||||
const out = {};
|
||||
switch (entryClean) {
|
||||
case "cartographer's tools or navigator's tools":
|
||||
out.choose = {from: ["navigator's tools", "cartographer's tools"]};
|
||||
break;
|
||||
case "disguise kit, and artisan's tools or gaming set":
|
||||
out["disguise kit"] = true;
|
||||
out.choose = {from: ["artisan's tools", "gaming set"]};
|
||||
break;
|
||||
case "any one musical instrument or gaming set of your choice, likely something native to your homeland":
|
||||
out.choose = {from: ["musical instrument", "gaming set"]};
|
||||
break;
|
||||
case "your choice of a gaming set or a musical instrument":
|
||||
out.choose = {from: ["musical instrument", "gaming set"]};
|
||||
break;
|
||||
case "musical instrument or artisan's tools":
|
||||
out.choose = {from: ["musical instrument", "artisan's tools"]};
|
||||
break;
|
||||
case "one type of artistic artisan's tools and one musical instrument":
|
||||
out["artisan's tools"] = true;
|
||||
out["musical instrument"] = true;
|
||||
break;
|
||||
case "choose two from among gaming set, one musical instrument, and thieves' tools":
|
||||
out.choose = {
|
||||
from: ["gaming set", "musical instrument", "thieves' tools"],
|
||||
count: 2,
|
||||
};
|
||||
break;
|
||||
case "artisan's tools, or navigator's tools, or an additional language":
|
||||
out.choose = {from: ["artisan's tools", "navigator's tools"]};
|
||||
break;
|
||||
case "gaming set or musical instrument":
|
||||
out.choose = {from: ["gaming set", "musical instrument"]};
|
||||
break;
|
||||
case "calligrapher's supplies or alchemist's supplies":
|
||||
out.choose = {from: ["calligrapher's supplies", "alchemist's supplies"]};
|
||||
break;
|
||||
default:
|
||||
cbWarning(`(${bg.name}) Tool proficiencies require manual tagging in "${entry}"`);
|
||||
}
|
||||
bg.toolProficiencies = [out];
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSpecial) {
|
||||
cbWarning(`(${bg.name}) Tool proficiencies require manual tagging in "${entry}"`);
|
||||
}
|
||||
}
|
||||
|
||||
static _doLanguageTag ({bg, list, cbWarning}) {
|
||||
const langProf = list.items.find(ent => BackgroundConverterConst.RE_NAME_LANGUAGES.test(ent.name));
|
||||
if (!langProf) return;
|
||||
|
||||
const languageProficiencies = this._getLanguageTags({langProf});
|
||||
if (!languageProficiencies) {
|
||||
cbWarning(`(${bg.name}) Language proficiencies require manual tagging in "${langProf.entry}"`);
|
||||
return;
|
||||
}
|
||||
bg.languageProficiencies = languageProficiencies;
|
||||
}
|
||||
|
||||
static _getLanguageTags ({langProf}) {
|
||||
langProf.entry = langProf.entry
|
||||
.replace(/\bElven\b/, "Elvish");
|
||||
|
||||
let str = langProf.entry
|
||||
.replace(/\([^)]+ recommended\)$/, "")
|
||||
.trim();
|
||||
|
||||
const reStrLanguage = `(${Parser.LANGUAGES_ALL.map(it => it.escapeRegexp()).join("|")})`;
|
||||
|
||||
const mSingle = new RegExp(`^${reStrLanguage}$`, "i").exec(str);
|
||||
if (mSingle) return [{[mSingle[1].toLowerCase()]: true}];
|
||||
|
||||
const mDoubleAnd = new RegExp(`^${reStrLanguage} and ${reStrLanguage}$`, "i").exec(str);
|
||||
if (mDoubleAnd) return [{[mDoubleAnd[1].toLowerCase()]: true, [mDoubleAnd[2].toLowerCase()]: true}];
|
||||
|
||||
const mDoubleAndChoose = new RegExp(`^${reStrLanguage} and one other language of your choice$`, "i").exec(str);
|
||||
if (mDoubleAndChoose) return [{[mDoubleAndChoose[1].toLowerCase()]: true, "anyStandard": true}];
|
||||
|
||||
const mDoubleOr = new RegExp(`^${reStrLanguage} or ${reStrLanguage}$`, "i").exec(str);
|
||||
if (mDoubleOr) return [{[mDoubleOr[1].toLowerCase()]: true}, {[mDoubleOr[2].toLowerCase()]: true}];
|
||||
|
||||
const mNumAny = /^(?:any )?((?<count>one|two) )?of your choice$/i.exec(str);
|
||||
if (mNumAny) return [{"anyStandard": Parser.textToNumber(mNumAny.groups?.count || "one")}];
|
||||
|
||||
const mNumExotic = /^(?:(?:any|choose) )?((?<count>one|two) )?exotic language(?: \([^)]+\))?$/i.exec(str);
|
||||
if (mNumExotic) return [{"anyExotic": Parser.textToNumber(mNumExotic.groups?.count || "one")}];
|
||||
|
||||
const mSingleOrAlternate = new RegExp(`^${reStrLanguage} or one of your choice if you already speak ${reStrLanguage}$`, "i").exec(str);
|
||||
if (mSingleOrAlternate) return [{[mSingle[1].toLowerCase()]: true}, {"anyStandard": 1}];
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.BackgroundSkillToolLanguageTag = BackgroundSkillToolLanguageTag;
|
||||
Reference in New Issue
Block a user