feat: culture description generator
This commit is contained in:
+69
-2
@@ -2,6 +2,7 @@ import { isNotNullish } from "@elyukai/utils/nullable"
|
||||
import { assertExhaustive } from "@elyukai/utils/typeSafety"
|
||||
import { deepEqual } from "@optolith/helpers/compare"
|
||||
import { MessageFormat } from "messageformat"
|
||||
import type { MessageValue } from "messageformat/functions"
|
||||
import { findPackageJSON } from "node:module"
|
||||
import { dirname, join } from "node:path"
|
||||
import { argv } from "node:process"
|
||||
@@ -76,6 +77,69 @@ const localeEnv: LocaleEnvironment = {
|
||||
translate: (key, ...rest) =>
|
||||
new MessageFormat(localeId, localeInstance.translations?.[key] ?? key, {
|
||||
bidiIsolation: "none",
|
||||
functions: {
|
||||
list: (ctx, options, input): MessageValue<"list"> => {
|
||||
if (!Array.isArray(input)) {
|
||||
ctx.onError(
|
||||
new RangeError("Input for list function must be an array"),
|
||||
)
|
||||
return {
|
||||
type: "list",
|
||||
options,
|
||||
}
|
||||
} else {
|
||||
switch (options.type) {
|
||||
case "conjunction": {
|
||||
const value = conjunctionListFormat.format(input)
|
||||
return {
|
||||
type: "list",
|
||||
options,
|
||||
toString() {
|
||||
return value
|
||||
},
|
||||
valueOf() {
|
||||
return value
|
||||
},
|
||||
}
|
||||
}
|
||||
case "disjunction": {
|
||||
const value = disjunctionListFormat.format(input)
|
||||
return {
|
||||
type: "list",
|
||||
options,
|
||||
toString() {
|
||||
return value
|
||||
},
|
||||
valueOf() {
|
||||
return value
|
||||
},
|
||||
}
|
||||
}
|
||||
case "unit": {
|
||||
const value = unitListFormat.format(input)
|
||||
return {
|
||||
type: "list",
|
||||
options,
|
||||
toString() {
|
||||
return value
|
||||
},
|
||||
valueOf() {
|
||||
return value
|
||||
},
|
||||
}
|
||||
}
|
||||
default:
|
||||
ctx.onError(
|
||||
new RangeError("Unsupported list type: ${options.type}"),
|
||||
)
|
||||
return {
|
||||
type: "list",
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}).format(rest[0] as Record<string, unknown> | undefined),
|
||||
translateMap: translations => translations?.[localeId],
|
||||
measurementAdjustments: {
|
||||
@@ -196,7 +260,7 @@ const logSection = (
|
||||
switch (section.type) {
|
||||
case "labeled":
|
||||
console.log(indent + styleText("bold", section.label))
|
||||
logSection(section.value, level, indent + " ")
|
||||
logSection(section.value, level, indent)
|
||||
break
|
||||
case "plain":
|
||||
console.log(indent + section.text)
|
||||
@@ -216,7 +280,10 @@ const logSection = (
|
||||
styleText(actualLevel > 1 ? "italic" : "bold", item.label + ":"),
|
||||
)
|
||||
if (Array.isArray(item.value)) {
|
||||
item.value.forEach(subsection => {
|
||||
item.value.forEach((subsection, subsectionIndex) => {
|
||||
if (subsectionIndex > 0) {
|
||||
console.log()
|
||||
}
|
||||
logSection(subsection, actualLevel + 1, listIndent + " ")
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,692 @@
|
||||
import { allSame } from "@elyukai/utils/array/filters"
|
||||
import { ensureNonEmpty, isNotEmpty } from "@elyukai/utils/array/nonEmpty"
|
||||
import { sumWith } from "@elyukai/utils/array/reductions"
|
||||
import { equal } from "@elyukai/utils/equality"
|
||||
import { identity, on } from "@elyukai/utils/function"
|
||||
import { isNotNullish, mapNullable } from "@elyukai/utils/nullable"
|
||||
import { Reader } from "@elyukai/utils/reader"
|
||||
import { sign } from "@elyukai/utils/string/number"
|
||||
import { assertExhaustive } from "@elyukai/utils/typeSafety"
|
||||
import type {
|
||||
BlessedTraditionConstraint,
|
||||
CommonNames,
|
||||
CommonProfessionConstraints,
|
||||
CommonProfessionConstraintsOperation,
|
||||
CommonProfessions,
|
||||
CulturalPackageItem,
|
||||
MagicalTraditionConstraint,
|
||||
ProfessionConstraint,
|
||||
Rarity,
|
||||
Skill_ID,
|
||||
Weighted,
|
||||
} from "optolith-database-schema/gen"
|
||||
import { createEntityDescriptionCreator } from "../creator.js"
|
||||
import type {
|
||||
GetAllChildInstancesForParent,
|
||||
GetInstanceById,
|
||||
} from "../helpers/getTypes.js"
|
||||
import type { TranslationKeysWithoutParams } from "../helpers/translate.js"
|
||||
import type {
|
||||
RawDefinitionListEntityDescriptionSectionItem,
|
||||
RawEntityDescription,
|
||||
RawEntityDescriptionSectionContent,
|
||||
RawNestedDefinitionListEntityDescriptionSection,
|
||||
} from "../index.js"
|
||||
import {
|
||||
renderCommonnessRatedAdvantagesOrDisadvantages,
|
||||
renderValueWithPossibleTranslation,
|
||||
} from "./partial/commonnessRatedAdvantagesAndDisadvantages.js"
|
||||
import { getProfessionName } from "./partial/professions.js"
|
||||
import { parensIf } from "./partial/rated/activatable/parensIf.js"
|
||||
import { calculateAdventurePointsFromImprovementCost } from "./partial/rated/improvementCost.js"
|
||||
import {
|
||||
translateR,
|
||||
type EnvMap,
|
||||
type StdEnv,
|
||||
type StdReader,
|
||||
} from "./partial/reader.js"
|
||||
import { MISSING_VALUE } from "./partial/unknown.js"
|
||||
|
||||
const renderListOperation = (
|
||||
operation: CommonProfessionConstraintsOperation,
|
||||
list: string[],
|
||||
): StdReader<string, "t"> => {
|
||||
switch (operation.kind) {
|
||||
case "Intersection":
|
||||
return Reader.of(list.join(", "))
|
||||
case "Difference":
|
||||
return translateR(
|
||||
"all but {$excludedProfessions :list type=conjunction}",
|
||||
{ excludedProfessions: list },
|
||||
)
|
||||
default:
|
||||
return assertExhaustive(operation)
|
||||
}
|
||||
}
|
||||
|
||||
const renderCommonProfessionConstraints = <T>(
|
||||
constraints: CommonProfessionConstraints<T>,
|
||||
renderConstraint: (
|
||||
constraint: T,
|
||||
) => StdReader<
|
||||
string | undefined,
|
||||
"t" | "tm" | "lc" | "ibi",
|
||||
"ProfessionVariant" | "BlessedTradition" | "MagicalTradition"
|
||||
>,
|
||||
): StdReader<
|
||||
string,
|
||||
"t" | "tm" | "lc" | "ibi",
|
||||
"ProfessionVariant" | "BlessedTradition" | "MagicalTradition"
|
||||
> =>
|
||||
Reader.asks(({ localeCompare }: StdEnv<"lc">) =>
|
||||
Reader.sequence(constraints.constraints.map(renderConstraint)).thenW(
|
||||
constraintValues =>
|
||||
renderListOperation(
|
||||
constraints.operation,
|
||||
constraintValues.filter(isNotNullish).toSorted(localeCompare),
|
||||
),
|
||||
),
|
||||
).thenW(identity)
|
||||
|
||||
const renderCommonProfessionGroup = <T>(
|
||||
label: TranslationKeysWithoutParams,
|
||||
constraints: CommonProfessionConstraints<T> | undefined,
|
||||
renderConstraint: (
|
||||
constraint: T,
|
||||
) => StdReader<
|
||||
string | undefined,
|
||||
"t" | "tm" | "lc" | "ibi",
|
||||
"ProfessionVariant" | "BlessedTradition" | "MagicalTradition"
|
||||
>,
|
||||
) =>
|
||||
translateR(label).thenW(translatedLabel =>
|
||||
(constraints === undefined
|
||||
? Reader.of("—")
|
||||
: renderCommonProfessionConstraints(constraints, renderConstraint)
|
||||
).map(translatedValue => ({
|
||||
label: translatedLabel,
|
||||
value: translatedValue,
|
||||
})),
|
||||
)
|
||||
|
||||
const renderRarity = (
|
||||
rarity: Rarity | undefined,
|
||||
): StdReader<string | undefined, "t"> => {
|
||||
if (rarity === undefined) {
|
||||
return Reader.of(undefined)
|
||||
}
|
||||
|
||||
switch (rarity.kind) {
|
||||
case "Rare":
|
||||
return translateR("rare")
|
||||
case "VeryRare":
|
||||
return translateR("very rare")
|
||||
default:
|
||||
return assertExhaustive(rarity)
|
||||
}
|
||||
}
|
||||
|
||||
const renderWeighted = <ID extends string>(
|
||||
weightedVariants: Weighted<ID> | undefined,
|
||||
getName: (id: ID) => string | undefined,
|
||||
): StdReader<string | undefined, "t" | "lc" | "ibi"> => {
|
||||
if (weightedVariants === undefined) {
|
||||
return Reader.of(undefined)
|
||||
}
|
||||
|
||||
return Reader.asks(({ translate, localeCompare }) => {
|
||||
const variants = ensureNonEmpty(
|
||||
weightedVariants.elements
|
||||
.map(getName)
|
||||
.filter(isNotNullish)
|
||||
.toSorted(localeCompare),
|
||||
)
|
||||
|
||||
if (variants === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
switch (weightedVariants.weight.kind) {
|
||||
case "Mostly":
|
||||
return translate("mostly {$variants :list type=conjunction}", {
|
||||
variants,
|
||||
})
|
||||
case "Only":
|
||||
return translate("only {$variants :list type=conjunction}", {
|
||||
variants,
|
||||
})
|
||||
default:
|
||||
return assertExhaustive(weightedVariants.weight)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderProfessionConstraint = (
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<"ProfessionVersion">,
|
||||
constraint: ProfessionConstraint,
|
||||
) =>
|
||||
Reader.asks(({ translateMap }: StdEnv<"tm">) =>
|
||||
getProfessionName(
|
||||
translateMap,
|
||||
getChildInstancesForInstanceId,
|
||||
constraint.id,
|
||||
),
|
||||
).thenW(baseName =>
|
||||
baseName === undefined
|
||||
? Reader.of(undefined)
|
||||
: Reader.sequence<
|
||||
StdEnv<"t" | "tm" | "lc" | "ibi", "ProfessionVariant">,
|
||||
string | undefined
|
||||
>([
|
||||
renderRarity(constraint.rarity),
|
||||
Reader.asks(
|
||||
({
|
||||
translateMap,
|
||||
getInstanceById,
|
||||
}: StdEnv<"tm" | "ibi", "ProfessionVariant">) =>
|
||||
renderWeighted(
|
||||
constraint.weighted_variants,
|
||||
variantId =>
|
||||
translateMap(
|
||||
getInstanceById("ProfessionVariant", variantId)
|
||||
?.translations,
|
||||
)?.name.default,
|
||||
),
|
||||
).thenW(identity),
|
||||
]).map(
|
||||
notes =>
|
||||
baseName +
|
||||
parensIf(ensureNonEmpty(notes.filter(isNotNullish))?.join("; ")),
|
||||
),
|
||||
)
|
||||
|
||||
const renderTraditionConstraint = <
|
||||
E extends "MagicalTradition" | "BlessedTradition",
|
||||
>(
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<"ProfessionVersion">,
|
||||
entity: E,
|
||||
constraint: MagicalTraditionConstraint | BlessedTraditionConstraint,
|
||||
): StdReader<string | undefined, "t" | "tm" | "lc" | "ibi", E> =>
|
||||
Reader.asks(({ translateMap, getInstanceById }: StdEnv<"tm" | "ibi", E>) => {
|
||||
const translation = translateMap<{
|
||||
name: string
|
||||
nameOfBlessedOnes?: string
|
||||
}>(getInstanceById(entity, constraint.id)?.translations)
|
||||
|
||||
return translation?.nameOfBlessedOnes ?? translation?.name
|
||||
}).thenW(baseName =>
|
||||
baseName === undefined
|
||||
? Reader.of(undefined)
|
||||
: Reader.sequence([
|
||||
renderRarity(constraint.rarity),
|
||||
Reader.asks(({ translateMap }: StdEnv<"tm">) =>
|
||||
renderWeighted(constraint.weighted_professions, profId =>
|
||||
getProfessionName(
|
||||
translateMap,
|
||||
getChildInstancesForInstanceId,
|
||||
profId,
|
||||
),
|
||||
),
|
||||
).thenW(identity),
|
||||
]).map(
|
||||
notes =>
|
||||
baseName +
|
||||
parensIf(ensureNonEmpty(notes.filter(isNotNullish))?.join("; ")),
|
||||
),
|
||||
)
|
||||
|
||||
const renderCommonProfessions = (
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<"ProfessionVersion">,
|
||||
commonProfessions: CommonProfessions,
|
||||
): StdReader<
|
||||
| string
|
||||
| [
|
||||
RawEntityDescriptionSectionContent<RawNestedDefinitionListEntityDescriptionSection>,
|
||||
],
|
||||
"t" | "tm" | "lc" | "ibi",
|
||||
"ProfessionVariant" | "BlessedTradition" | "MagicalTradition"
|
||||
> => {
|
||||
switch (commonProfessions.kind) {
|
||||
case "Plain":
|
||||
return renderCommonProfessionConstraints(
|
||||
commonProfessions.Plain,
|
||||
profId =>
|
||||
Reader.asks(({ translateMap }) =>
|
||||
getProfessionName(
|
||||
translateMap,
|
||||
getChildInstancesForInstanceId,
|
||||
profId,
|
||||
),
|
||||
),
|
||||
)
|
||||
case "Grouped":
|
||||
return renderCommonProfessionGroup(
|
||||
"Mundane Professions",
|
||||
commonProfessions.Grouped.mundane,
|
||||
constraint =>
|
||||
// switch (constraint.kind) {
|
||||
// case "Profession":
|
||||
renderProfessionConstraint(
|
||||
getChildInstancesForInstanceId,
|
||||
constraint.Profession,
|
||||
),
|
||||
// case "ProfessionSubgroup":
|
||||
// switch (constraint.ProfessionSubgroup.kind) {
|
||||
// case "Profane":
|
||||
// case "Fighter":
|
||||
// case "Religious":
|
||||
// return Reader.of(UNHANDLED_VALUE)
|
||||
// default:
|
||||
// return assertExhaustive(constraint.ProfessionSubgroup)
|
||||
// }
|
||||
// default:
|
||||
// return assertExhaustive(constraint)
|
||||
// }
|
||||
).then(mundane =>
|
||||
renderCommonProfessionGroup(
|
||||
"Magic Professions",
|
||||
commonProfessions.Grouped.magic,
|
||||
constraint => {
|
||||
switch (constraint.kind) {
|
||||
case "Profession":
|
||||
return renderProfessionConstraint(
|
||||
getChildInstancesForInstanceId,
|
||||
constraint.Profession,
|
||||
)
|
||||
case "Tradition":
|
||||
return renderTraditionConstraint(
|
||||
getChildInstancesForInstanceId,
|
||||
"MagicalTradition",
|
||||
constraint.Tradition,
|
||||
)
|
||||
case "MagicDilettante":
|
||||
return translateR("Magic Dilettante")
|
||||
default:
|
||||
return assertExhaustive(constraint)
|
||||
}
|
||||
},
|
||||
).then(magic =>
|
||||
renderCommonProfessionGroup(
|
||||
"Blessed Professions",
|
||||
commonProfessions.Grouped.blessed,
|
||||
constraint =>
|
||||
renderTraditionConstraint(
|
||||
getChildInstancesForInstanceId,
|
||||
"BlessedTradition",
|
||||
constraint.Tradition,
|
||||
),
|
||||
).map(blessed => [
|
||||
{
|
||||
type: "definitionList",
|
||||
style: "nested",
|
||||
items: [mundane, magic, blessed],
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
default:
|
||||
return assertExhaustive(commonProfessions)
|
||||
}
|
||||
}
|
||||
|
||||
const renderCommonSkills = (
|
||||
items: Skill_ID[] | undefined,
|
||||
): StdReader<string, "t" | "tm" | "lc" | "ibi", "Skill"> =>
|
||||
Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) =>
|
||||
items === undefined || !isNotEmpty(items)
|
||||
? translate("none")
|
||||
: items
|
||||
.map(
|
||||
itemId =>
|
||||
translateMap(getInstanceById("Skill", itemId)?.translations)
|
||||
?.name,
|
||||
)
|
||||
.filter(isNotNullish)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
)
|
||||
|
||||
const renderCommonNames = (
|
||||
commonNames: CommonNames,
|
||||
): StdReader<
|
||||
(
|
||||
| RawEntityDescriptionSectionContent<RawNestedDefinitionListEntityDescriptionSection>
|
||||
| undefined
|
||||
)[],
|
||||
"lc"
|
||||
> =>
|
||||
Reader.asks(({ localeCompare }) => {
|
||||
const groups = [
|
||||
...(commonNames.first_name_groups ?? []),
|
||||
...(commonNames.last_name_groups ?? []),
|
||||
].map(
|
||||
(group): RawDefinitionListEntityDescriptionSectionItem => ({
|
||||
label: group.label,
|
||||
value: group.names
|
||||
.map(name => name.name + parensIf(name.note))
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
}),
|
||||
)
|
||||
|
||||
const specialRules = commonNames.naming_rules
|
||||
|
||||
return [
|
||||
mapNullable(ensureNonEmpty(groups), renderedGroups => ({
|
||||
type: "definitionList",
|
||||
style: "nested",
|
||||
items: renderedGroups,
|
||||
})),
|
||||
mapNullable(specialRules, rules => ({
|
||||
type: "plain",
|
||||
text: rules,
|
||||
})),
|
||||
]
|
||||
})
|
||||
|
||||
const renderCulturalPackage = (
|
||||
items: CulturalPackageItem[],
|
||||
): StdReader<
|
||||
{ text: string; apValue: number },
|
||||
"t" | "tm" | "lc" | "ibi",
|
||||
"Skill"
|
||||
> =>
|
||||
Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) => {
|
||||
if (!isNotEmpty(items)) {
|
||||
return { text: translate("none"), apValue: 0 }
|
||||
}
|
||||
|
||||
const processedItems = items.map(
|
||||
(item): [text: string, apValue: number] => {
|
||||
const instance = getInstanceById("Skill", item.id)
|
||||
const instanceTranslation = translateMap(instance?.translations)
|
||||
|
||||
if (instance === undefined || instanceTranslation === undefined) {
|
||||
return [MISSING_VALUE, 0]
|
||||
}
|
||||
|
||||
return [
|
||||
`${instanceTranslation.name ?? MISSING_VALUE} ${sign(item.points)}`,
|
||||
calculateAdventurePointsFromImprovementCost(
|
||||
instance.improvement_cost,
|
||||
[1, item.points],
|
||||
),
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
text: processedItems
|
||||
.map(([text]) => text)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
apValue: sumWith(processedItems, ([, apValue]) => apValue),
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get a JSON representation of the rules text for a culture.
|
||||
*/
|
||||
export const getCultureEntityDescription = createEntityDescriptionCreator<
|
||||
"Culture",
|
||||
{
|
||||
getInstanceById: GetInstanceById<
|
||||
| "Publication"
|
||||
| "Advantage"
|
||||
| "Disadvantage"
|
||||
| "Skill"
|
||||
| "Language"
|
||||
| "LanguageSpecialization"
|
||||
| "Script"
|
||||
| "SocialStatus"
|
||||
| "ProfessionVariant"
|
||||
| "MagicalTradition"
|
||||
| "BlessedTradition"
|
||||
>
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<"ProfessionVersion">
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ getInstanceById, getChildInstancesForInstanceId },
|
||||
{ translate, translateMap, compare: localeCompare, join: localeJoin },
|
||||
{ content: entry },
|
||||
): RawEntityDescription | undefined => {
|
||||
const translation = translateMap(entry.translations)
|
||||
|
||||
if (translation === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const env = {
|
||||
translate,
|
||||
translateMap,
|
||||
localeCompare,
|
||||
getInstanceById,
|
||||
} satisfies Partial<EnvMap>
|
||||
|
||||
const { text: culturePackageText, apValue: culturalPackageApValue } =
|
||||
renderCulturalPackage(entry.cultural_package).run(env)
|
||||
|
||||
return {
|
||||
title: translation.name,
|
||||
className: "culture",
|
||||
body: [
|
||||
{
|
||||
type: "definitionList",
|
||||
items: [
|
||||
{
|
||||
label: translate("Language"),
|
||||
value: localeJoin(
|
||||
entry.language
|
||||
.map(lang => {
|
||||
const language = getInstanceById("Language", lang.id)
|
||||
const languageTranslation = translateMap(
|
||||
language?.translations,
|
||||
)
|
||||
|
||||
if (
|
||||
language === undefined ||
|
||||
languageTranslation === undefined
|
||||
) {
|
||||
return MISSING_VALUE
|
||||
}
|
||||
|
||||
return (
|
||||
languageTranslation.name +
|
||||
parensIf(
|
||||
[
|
||||
translateMap(
|
||||
language.customSpecializations?.translations,
|
||||
)?.description,
|
||||
...(lang.specializations?.map(
|
||||
specId =>
|
||||
translateMap(
|
||||
getInstanceById(
|
||||
"LanguageSpecialization",
|
||||
specId,
|
||||
)?.translations,
|
||||
)?.name,
|
||||
) ?? []),
|
||||
]
|
||||
.filter(isNotNullish)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
)
|
||||
)
|
||||
})
|
||||
.toSorted(localeCompare),
|
||||
"disjunction",
|
||||
),
|
||||
},
|
||||
{
|
||||
label: translate("Script"),
|
||||
value:
|
||||
entry.script === undefined
|
||||
? translate("none")
|
||||
: (() => {
|
||||
const processedScripts = entry.script.map(
|
||||
(scriptId): [string, number] => {
|
||||
const script = getInstanceById("Script", scriptId)
|
||||
const scriptTranslation = translateMap(
|
||||
script?.translations,
|
||||
)
|
||||
|
||||
if (
|
||||
script === undefined ||
|
||||
scriptTranslation === undefined
|
||||
) {
|
||||
return [MISSING_VALUE, 0]
|
||||
}
|
||||
|
||||
return [scriptTranslation.name, script.ap_value ?? 0]
|
||||
},
|
||||
)
|
||||
|
||||
if (!isNotEmpty(processedScripts)) {
|
||||
return translate("none")
|
||||
}
|
||||
|
||||
if (
|
||||
allSame(
|
||||
processedScripts,
|
||||
on(([, apValue]) => apValue, equal),
|
||||
)
|
||||
) {
|
||||
return (
|
||||
localeJoin(
|
||||
processedScripts
|
||||
.map(([name]) => name)
|
||||
.toSorted(localeCompare),
|
||||
"disjunction",
|
||||
) +
|
||||
parensIf(
|
||||
translate("{$value} AP", {
|
||||
value: processedScripts[0][1],
|
||||
}),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return localeJoin(
|
||||
processedScripts
|
||||
.map(
|
||||
([name, apValue]) =>
|
||||
name +
|
||||
parensIf(
|
||||
translate("{$value} AP", {
|
||||
value: apValue,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.toSorted(localeCompare),
|
||||
"disjunction",
|
||||
)
|
||||
}
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: translate("Area Knowledge"),
|
||||
value:
|
||||
translation.area_knowledge.description +
|
||||
(translation.area_knowledge.examples === undefined
|
||||
? ""
|
||||
: parensIf(
|
||||
translate("for example, {$examples}", {
|
||||
examples: translation.area_knowledge.examples
|
||||
.map(example => example.area)
|
||||
.join(", "),
|
||||
}),
|
||||
)),
|
||||
},
|
||||
{
|
||||
label: translate("Social Status"),
|
||||
value: entry.social_status
|
||||
.map(
|
||||
status =>
|
||||
translateMap(
|
||||
getInstanceById("SocialStatus", status)?.translations,
|
||||
)?.name ?? MISSING_VALUE,
|
||||
)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
},
|
||||
{
|
||||
label: translate("Common Professions"),
|
||||
value: renderCommonProfessions(
|
||||
getChildInstancesForInstanceId,
|
||||
entry.common_professions,
|
||||
).run(env),
|
||||
},
|
||||
renderValueWithPossibleTranslation(
|
||||
"Common Advantages",
|
||||
entry.common_advantages,
|
||||
values =>
|
||||
renderCommonnessRatedAdvantagesOrDisadvantages(
|
||||
"Advantage",
|
||||
values,
|
||||
).run(env),
|
||||
translation.common_advantages,
|
||||
).run(env),
|
||||
renderValueWithPossibleTranslation(
|
||||
"Common Disadvantages",
|
||||
entry.common_disadvantages,
|
||||
values =>
|
||||
renderCommonnessRatedAdvantagesOrDisadvantages(
|
||||
"Disadvantage",
|
||||
values,
|
||||
).run(env),
|
||||
translation.common_disadvantages,
|
||||
).run(env),
|
||||
renderValueWithPossibleTranslation(
|
||||
"Uncommon Advantages",
|
||||
entry.uncommon_advantages,
|
||||
values =>
|
||||
renderCommonnessRatedAdvantagesOrDisadvantages(
|
||||
"Advantage",
|
||||
values,
|
||||
).run(env),
|
||||
translation.uncommon_advantages,
|
||||
).run(env),
|
||||
renderValueWithPossibleTranslation(
|
||||
"Uncommon Disadvantages",
|
||||
entry.uncommon_disadvantages,
|
||||
values =>
|
||||
renderCommonnessRatedAdvantagesOrDisadvantages(
|
||||
"Disadvantage",
|
||||
values,
|
||||
).run(env),
|
||||
translation.uncommon_disadvantages,
|
||||
).run(env),
|
||||
{
|
||||
label: translate("Common Skills"),
|
||||
value: renderCommonSkills(entry.common_skills).run(env),
|
||||
},
|
||||
{
|
||||
label: translate("Uncommon Skills"),
|
||||
value: renderCommonSkills(entry.uncommon_skills).run(env),
|
||||
},
|
||||
{
|
||||
label: translate("Common Names"),
|
||||
value: renderCommonNames(translation.common_names).run(env),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "labeled",
|
||||
label:
|
||||
translate("Cultural Package {$cultureName}", {
|
||||
cultureName: translation.name,
|
||||
}) +
|
||||
parensIf(
|
||||
translate("{$value} AP", { value: culturalPackageApValue }),
|
||||
),
|
||||
value: {
|
||||
type: "plain",
|
||||
text: culturePackageText,
|
||||
},
|
||||
},
|
||||
],
|
||||
errata: translation.errata,
|
||||
references: entry.src,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,5 +1,4 @@
|
||||
import { partition } from "@elyukai/utils/array/groups"
|
||||
import { reduceWhile } from "@elyukai/utils/array/reductions"
|
||||
import { Dictionary } from "@elyukai/utils/dictionary"
|
||||
import { deepEqual } from "@elyukai/utils/equality"
|
||||
import { sign } from "@elyukai/utils/string/number"
|
||||
@@ -28,6 +27,7 @@ import type {
|
||||
LabeledEntityDescriptionSection,
|
||||
RawEntityDescriptionSectionContent,
|
||||
} from "../index.js"
|
||||
import { getBaseProfessionPackageForCurriculum } from "./partial/professions.js"
|
||||
import { parensIf } from "./partial/rated/activatable/parensIf.js"
|
||||
import { MISSING_VALUE } from "./partial/unknown.js"
|
||||
|
||||
@@ -264,38 +264,6 @@ const renderSpellworkAdjustment = (
|
||||
)?.name,
|
||||
)}`
|
||||
|
||||
const getBaseProfessionPackage = (
|
||||
getAllInstances: GetAllInstances<"Profession">,
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<
|
||||
"ProfessionVersion" | "ProfessionPackage"
|
||||
>,
|
||||
idMap: IdMap,
|
||||
curriculumId: string,
|
||||
) => {
|
||||
const baseProfession = getAllInstances("Profession").find(
|
||||
profession =>
|
||||
profession.content.group.kind === "Magical" &&
|
||||
profession.content.group.Magical.curriculum === curriculumId,
|
||||
)
|
||||
|
||||
if (baseProfession === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return reduceWhile(
|
||||
getChildInstancesForInstanceId("ProfessionVersion", baseProfession.id),
|
||||
(_acc: { id: string; content: ProfessionPackage } | undefined, version) =>
|
||||
getChildInstancesForInstanceId("ProfessionPackage", version.id).find(
|
||||
professionPackage =>
|
||||
professionPackage.content.experience_level === undefined ||
|
||||
professionPackage.content.experience_level ===
|
||||
idMap.ExperienceLevel.Experienced,
|
||||
),
|
||||
isNotNullish,
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
|
||||
const renderAbilityAdjustmentName = (
|
||||
translateMap: TranslateMap,
|
||||
getInstanceById: GetInstanceById<
|
||||
@@ -497,7 +465,7 @@ export const getCurriculumEntityDescription = createEntityDescriptionCreator<
|
||||
return undefined
|
||||
}
|
||||
|
||||
const baseProfessionPackage = getBaseProfessionPackage(
|
||||
const baseProfessionPackage = getBaseProfessionPackageForCurriculum(
|
||||
getAllInstances,
|
||||
getChildInstancesForInstanceId,
|
||||
idMap,
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { isNotEmpty } from "@elyukai/utils/array/nonEmpty"
|
||||
import { mapNullable } from "@elyukai/utils/nullable"
|
||||
import { Reader } from "@elyukai/utils/reader"
|
||||
import { ensureNonEmpty } from "@optolith/helpers/array"
|
||||
import type { CommonnessRatedAdvantageDisadvantage } from "optolith-database-schema/gen"
|
||||
import type { GetInstanceById } from "../../helpers/getTypes.js"
|
||||
import type {
|
||||
TranslateMap,
|
||||
TranslationKeysWithoutParams,
|
||||
} from "../../helpers/translate.js"
|
||||
import type { RawDefinitionListEntityDescriptionSectionItem } from "../../index.js"
|
||||
import { parensIf } from "./rated/activatable/parensIf.js"
|
||||
import type { StdReader } from "./reader.js"
|
||||
import { MISSING_VALUE } from "./unknown.js"
|
||||
|
||||
const renderCommonnessRatedAdvantageOrDisadvantageName = <
|
||||
E extends "Advantage" | "Disadvantage",
|
||||
>(
|
||||
translateMap: TranslateMap,
|
||||
getInstanceById: GetInstanceById<E>,
|
||||
entity: E,
|
||||
item: CommonnessRatedAdvantageDisadvantage<string>,
|
||||
): string => {
|
||||
const instance = getInstanceById(entity, item.id)
|
||||
const instanceTranslation = translateMap(instance?.translations)
|
||||
const customTranslation = translateMap(item.translations)
|
||||
const name = instanceTranslation?.name_in_library ?? instanceTranslation?.name
|
||||
return name !== undefined &&
|
||||
customTranslation?.options !== undefined &&
|
||||
name.endsWith(")")
|
||||
? `${name.slice(0, -1)}; ${customTranslation.options})`
|
||||
: (name ?? MISSING_VALUE) + parensIf(customTranslation?.options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the names of either commonness-rated advantages or commonness-rated disadvantages.
|
||||
*/
|
||||
export const renderCommonnessRatedAdvantagesOrDisadvantages = <
|
||||
E extends "Advantage" | "Disadvantage",
|
||||
>(
|
||||
entity: E,
|
||||
items: CommonnessRatedAdvantageDisadvantage<string>[] | undefined,
|
||||
): StdReader<string, "t" | "tm" | "lc" | "ibi", E> =>
|
||||
Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) =>
|
||||
items === undefined || !isNotEmpty(items)
|
||||
? translate("none")
|
||||
: items
|
||||
.map(item =>
|
||||
renderCommonnessRatedAdvantageOrDisadvantageName(
|
||||
translateMap,
|
||||
getInstanceById,
|
||||
entity,
|
||||
item,
|
||||
),
|
||||
)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
)
|
||||
|
||||
/**
|
||||
* Render the names of commonness-rated advantages and disadvantages together.
|
||||
*/
|
||||
export const renderCommonnessRatedAdvantagesAndDisadvantages = (
|
||||
advantages: CommonnessRatedAdvantageDisadvantage<string>[] | undefined,
|
||||
disadvantages: CommonnessRatedAdvantageDisadvantage<string>[] | undefined,
|
||||
): StdReader<string, "t" | "tm" | "lc" | "ibi", "Advantage" | "Disadvantage"> =>
|
||||
Reader.asks(
|
||||
({ translate, translateMap, getInstanceById, localeCompare }) =>
|
||||
mapNullable(
|
||||
ensureNonEmpty(
|
||||
[
|
||||
["Advantage", advantages] as const,
|
||||
["Disadvantage", disadvantages] as const,
|
||||
].flatMap(
|
||||
([entity, items]) =>
|
||||
items?.map(item =>
|
||||
renderCommonnessRatedAdvantageOrDisadvantageName(
|
||||
translateMap,
|
||||
getInstanceById,
|
||||
entity,
|
||||
item,
|
||||
),
|
||||
) ?? [],
|
||||
),
|
||||
),
|
||||
renderedItems => renderedItems.toSorted(localeCompare).join(", "),
|
||||
) ?? translate("none"),
|
||||
)
|
||||
|
||||
/**
|
||||
* Render a value with a possible translation, falling back to explicity rendering the value if no translation is available.
|
||||
*/
|
||||
export const renderValueWithPossibleTranslation = <T>(
|
||||
label: TranslationKeysWithoutParams,
|
||||
value: T,
|
||||
renderValue: (value: T) => string,
|
||||
valueTranslation: string | undefined,
|
||||
): StdReader<RawDefinitionListEntityDescriptionSectionItem, "t"> =>
|
||||
Reader.asks(
|
||||
({ translate }): RawDefinitionListEntityDescriptionSectionItem => ({
|
||||
label: translate(label),
|
||||
value: valueTranslation ?? renderValue(value),
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
import { reduceWhile } from "@elyukai/utils/array/reductions"
|
||||
import { isNotNullish } from "@elyukai/utils/nullable"
|
||||
import type {
|
||||
Profession_ID,
|
||||
ProfessionPackage,
|
||||
} from "optolith-database-schema/gen"
|
||||
import type {
|
||||
GetAllChildInstancesForParent,
|
||||
GetAllInstances,
|
||||
} from "../../helpers/getTypes.js"
|
||||
import type { TranslateMap } from "../../helpers/translate.js"
|
||||
import type { IdMap } from "../../index.js"
|
||||
|
||||
/**
|
||||
* Finds the base profession package for a given curriculum.
|
||||
*/
|
||||
export const getBaseProfessionPackageForCurriculum = (
|
||||
getAllInstances: GetAllInstances<"Profession">,
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<
|
||||
"ProfessionVersion" | "ProfessionPackage"
|
||||
>,
|
||||
idMap: IdMap,
|
||||
curriculumId: string,
|
||||
) => {
|
||||
const baseProfession = getAllInstances("Profession").find(
|
||||
profession =>
|
||||
profession.content.group.kind === "Magical" &&
|
||||
profession.content.group.Magical.curriculum === curriculumId,
|
||||
)
|
||||
|
||||
if (baseProfession === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return reduceWhile(
|
||||
getChildInstancesForInstanceId("ProfessionVersion", baseProfession.id),
|
||||
(_acc: { id: string; content: ProfessionPackage } | undefined, version) =>
|
||||
getChildInstancesForInstanceId("ProfessionPackage", version.id).find(
|
||||
professionPackage =>
|
||||
professionPackage.content.experience_level === undefined ||
|
||||
professionPackage.content.experience_level ===
|
||||
idMap.ExperienceLevel.Experienced,
|
||||
),
|
||||
isNotNullish,
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of a profession, regardless of the profession version.
|
||||
*/
|
||||
export const getProfessionName = (
|
||||
translateMap: TranslateMap,
|
||||
getChildInstancesForInstanceId: GetAllChildInstancesForParent<"ProfessionVersion">,
|
||||
professionId: Profession_ID,
|
||||
): string | undefined =>
|
||||
translateMap(
|
||||
getChildInstancesForInstanceId("ProfessionVersion", professionId)[0]
|
||||
?.content.translations,
|
||||
)?.name.default
|
||||
@@ -1,3 +1,5 @@
|
||||
import { rangeSize, type RangeBounds } from "@elyukai/utils/range"
|
||||
import { assertExhaustive } from "@elyukai/utils/typeSafety"
|
||||
import type { ImprovementCost } from "optolith-database-schema/gen"
|
||||
import { type RawDefinitionListEntityDescriptionSectionItem } from "../../../index.js"
|
||||
import { translateR, type StdReader } from "../reader.js"
|
||||
@@ -19,3 +21,73 @@ export const renderImprovementCost = (
|
||||
label,
|
||||
value: renderImprovementCostValue(improvementCost),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Calculates the adventure points for an improvement cost.
|
||||
* @throws {RangeError} if the rating range is invalid (max < min).
|
||||
*/
|
||||
export const calculateAdventurePointsFromImprovementCost = (
|
||||
improvementCost:
|
||||
| ImprovementCost
|
||||
| ImprovementCost["kind"]
|
||||
| { kind: "E" }
|
||||
| "E",
|
||||
rating: number | RangeBounds = 1,
|
||||
): number => {
|
||||
const normalized =
|
||||
typeof improvementCost === "string" ? improvementCost : improvementCost.kind
|
||||
|
||||
const base = (() => {
|
||||
switch (normalized) {
|
||||
case "A":
|
||||
return 1
|
||||
case "B":
|
||||
return 2
|
||||
case "C":
|
||||
return 3
|
||||
case "D":
|
||||
return 4
|
||||
case "E":
|
||||
return 15
|
||||
default:
|
||||
return assertExhaustive(normalized)
|
||||
}
|
||||
})()
|
||||
|
||||
const lastRatingOfSameValue = (() => {
|
||||
switch (normalized) {
|
||||
case "A":
|
||||
case "B":
|
||||
case "C":
|
||||
case "D":
|
||||
return 12
|
||||
case "E":
|
||||
return 14
|
||||
default:
|
||||
return assertExhaustive(normalized)
|
||||
}
|
||||
})()
|
||||
|
||||
if (typeof rating === "number") {
|
||||
return (
|
||||
base *
|
||||
(rating < lastRatingOfSameValue ? 1 : rating - lastRatingOfSameValue + 1)
|
||||
)
|
||||
} else {
|
||||
const [min, max] = rating
|
||||
|
||||
if (max < min) {
|
||||
throw new RangeError(`Invalid rating range: ${min} - ${max}`)
|
||||
}
|
||||
|
||||
const size = rangeSize(rating)
|
||||
const constantRangeSize = Math.min(size, lastRatingOfSameValue - min + 1)
|
||||
const variableRangeSize = size - constantRangeSize
|
||||
|
||||
const constantCost = base * constantRangeSize
|
||||
const variableCost =
|
||||
base * (((variableRangeSize + 1) * (variableRangeSize + 2)) / 2 - 1)
|
||||
|
||||
return constantCost + variableCost
|
||||
}
|
||||
}
|
||||
|
||||
+21
-111
@@ -1,14 +1,13 @@
|
||||
import { anySameIndices } from "@elyukai/utils/array/filters"
|
||||
import { ensureNonEmpty, isNotEmpty } from "@elyukai/utils/array/nonEmpty"
|
||||
import { isNotEmpty } from "@elyukai/utils/array/nonEmpty"
|
||||
import { deepEqual } from "@elyukai/utils/equality"
|
||||
import { on } from "@elyukai/utils/function"
|
||||
import { isNotNullish, mapNullable } from "@elyukai/utils/nullable"
|
||||
import { isNotNullish } from "@elyukai/utils/nullable"
|
||||
import { Reader } from "@elyukai/utils/reader"
|
||||
import { sign } from "@elyukai/utils/string/number"
|
||||
import type {
|
||||
AttributeAdjustments,
|
||||
AutomaticAdvantageDisadvantage,
|
||||
CommonnessRatedAdvantageDisadvantage,
|
||||
Culture_ID,
|
||||
RaceVariant,
|
||||
RaceVariantTranslation,
|
||||
@@ -19,12 +18,13 @@ import type {
|
||||
GetAllChildInstancesForParent,
|
||||
GetInstanceById,
|
||||
} from "../helpers/getTypes.js"
|
||||
import type {
|
||||
TranslateMap,
|
||||
TranslationKeysWithoutParams,
|
||||
} from "../helpers/translate.js"
|
||||
import type { TranslationKeysWithoutParams } from "../helpers/translate.js"
|
||||
import type { RawDefinitionListEntityDescriptionSectionItem } from "../index.js"
|
||||
import { parensIf } from "./partial/rated/activatable/parensIf.js"
|
||||
import {
|
||||
renderCommonnessRatedAdvantagesAndDisadvantages,
|
||||
renderCommonnessRatedAdvantagesOrDisadvantages,
|
||||
renderValueWithPossibleTranslation,
|
||||
} from "./partial/commonnessRatedAdvantagesAndDisadvantages.js"
|
||||
import type { EnvMap, StdReader } from "./partial/reader.js"
|
||||
import { MISSING_VALUE } from "./partial/unknown.js"
|
||||
|
||||
@@ -72,19 +72,6 @@ const renderAttributeAdjustmentsItem = (
|
||||
},
|
||||
)
|
||||
|
||||
const renderValueWithPossibleTranslation = <T>(
|
||||
label: TranslationKeysWithoutParams,
|
||||
value: T,
|
||||
renderValue: (value: T) => string,
|
||||
valueTranslation: string | undefined,
|
||||
): StdReader<RawDefinitionListEntityDescriptionSectionItem, "t"> =>
|
||||
Reader.asks(
|
||||
({ translate }): RawDefinitionListEntityDescriptionSectionItem => ({
|
||||
label: translate(label),
|
||||
value: valueTranslation ?? renderValue(value),
|
||||
}),
|
||||
)
|
||||
|
||||
const renderVariantValues = <T>(
|
||||
label: TranslationKeysWithoutParams,
|
||||
variants: RaceVariant[],
|
||||
@@ -161,74 +148,6 @@ const renderVariantValues = <T>(
|
||||
},
|
||||
)
|
||||
|
||||
const renderCommonnessRatedAdvantageOrDisadvantageName = <
|
||||
E extends "Advantage" | "Disadvantage",
|
||||
>(
|
||||
translateMap: TranslateMap,
|
||||
getInstanceById: GetInstanceById<E>,
|
||||
entity: E,
|
||||
item: CommonnessRatedAdvantageDisadvantage<string>,
|
||||
): string => {
|
||||
const instance = getInstanceById(entity, item.id)
|
||||
const instanceTranslation = translateMap(instance?.translations)
|
||||
const customTranslation = translateMap(item.translations)
|
||||
const name = instanceTranslation?.name_in_library ?? instanceTranslation?.name
|
||||
return name !== undefined &&
|
||||
customTranslation?.options !== undefined &&
|
||||
name.endsWith(")")
|
||||
? `${name.slice(0, -1)}; ${customTranslation.options})`
|
||||
: (name ?? MISSING_VALUE) + parensIf(customTranslation?.options)
|
||||
}
|
||||
|
||||
const renderCommonnessRatedAdvantagesOrDisadvantages = <
|
||||
E extends "Advantage" | "Disadvantage",
|
||||
>(
|
||||
entity: E,
|
||||
items: CommonnessRatedAdvantageDisadvantage<string>[] | undefined,
|
||||
): StdReader<string, "t" | "tm" | "lc" | "ibi", E> =>
|
||||
Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) =>
|
||||
items === undefined || !isNotEmpty(items)
|
||||
? translate("none")
|
||||
: items
|
||||
.map(item =>
|
||||
renderCommonnessRatedAdvantageOrDisadvantageName(
|
||||
translateMap,
|
||||
getInstanceById,
|
||||
entity,
|
||||
item,
|
||||
),
|
||||
)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
)
|
||||
|
||||
const renderCommonnessRatedAdvantagesAndDisadvantages = (
|
||||
advantages: CommonnessRatedAdvantageDisadvantage<string>[] | undefined,
|
||||
disadvantages: CommonnessRatedAdvantageDisadvantage<string>[] | undefined,
|
||||
): StdReader<string, "t" | "tm" | "lc" | "ibi", "Advantage" | "Disadvantage"> =>
|
||||
Reader.asks(
|
||||
({ translate, translateMap, getInstanceById, localeCompare }) =>
|
||||
mapNullable(
|
||||
ensureNonEmpty(
|
||||
[
|
||||
["Advantage", advantages] as const,
|
||||
["Disadvantage", disadvantages] as const,
|
||||
].flatMap(
|
||||
([entity, items]) =>
|
||||
items?.map(item =>
|
||||
renderCommonnessRatedAdvantageOrDisadvantageName(
|
||||
translateMap,
|
||||
getInstanceById,
|
||||
entity,
|
||||
item,
|
||||
),
|
||||
) ?? [],
|
||||
),
|
||||
),
|
||||
renderedItems => renderedItems.toSorted(localeCompare).join(", "),
|
||||
) ?? translate("none"),
|
||||
)
|
||||
|
||||
const renderAutomaticAdvantagesOrDisadvantages = <
|
||||
E extends "Advantage" | "Disadvantage",
|
||||
ID extends string,
|
||||
@@ -256,28 +175,19 @@ const renderAutomaticAdvantagesOrDisadvantages = <
|
||||
|
||||
const renderCommonCultures = (
|
||||
items: Culture_ID[] | undefined,
|
||||
): StdReader<string, "t" | "tm" | "lc" | "lj" | "ibi", "Culture"> =>
|
||||
Reader.asks(
|
||||
({
|
||||
translate,
|
||||
translateMap,
|
||||
getInstanceById,
|
||||
localeCompare,
|
||||
localeJoin,
|
||||
}) =>
|
||||
items === undefined || !isNotEmpty(items)
|
||||
? translate("none")
|
||||
: localeJoin(
|
||||
items
|
||||
.map(
|
||||
itemId =>
|
||||
translateMap(getInstanceById("Culture", itemId)?.translations)
|
||||
?.name,
|
||||
)
|
||||
.filter(isNotNullish)
|
||||
.toSorted(localeCompare),
|
||||
"conjunction",
|
||||
),
|
||||
): StdReader<string, "t" | "tm" | "lc" | "ibi", "Culture"> =>
|
||||
Reader.asks(({ translate, translateMap, getInstanceById, localeCompare }) =>
|
||||
items === undefined || !isNotEmpty(items)
|
||||
? translate("none")
|
||||
: items
|
||||
.map(
|
||||
itemId =>
|
||||
translateMap(getInstanceById("Culture", itemId)?.translations)
|
||||
?.name,
|
||||
)
|
||||
.filter(isNotNullish)
|
||||
.toSorted(localeCompare)
|
||||
.join(", "),
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import {
|
||||
getConditionEntityDescription,
|
||||
getMetaConditionEntityDescription,
|
||||
} from "./entities/condition.js"
|
||||
import { getCultureEntityDescription } from "./entities/culture.js"
|
||||
import { getCurriculumEntityDescription } from "./entities/curriculum.js"
|
||||
import { getDerivedCharacteristicEntityDescription } from "./entities/derivedCharacteristic.js"
|
||||
import { getDiseaseEntityDescription } from "./entities/disease.js"
|
||||
@@ -258,7 +259,7 @@ const registeredEntityDescriptionCreators = {
|
||||
ExperienceLevel: getExperienceLevelEntityDescription,
|
||||
DerivedCharacteristic: getDerivedCharacteristicEntityDescription,
|
||||
Race: getRaceEntityDescription,
|
||||
// Culture: getCultureEntityDescription,
|
||||
Culture: getCultureEntityDescription,
|
||||
// ProfessionVersion: getProfessionVersionEntityDescription,
|
||||
Advantage: getActivatableEntityDescription,
|
||||
Disadvantage: getActivatableEntityDescription,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { equal, throws } from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
import { calculateAdventurePointsFromImprovementCost } from "../../../../src/entities/partial/rated/improvementCost.js"
|
||||
|
||||
describe("calculateAdventurePointsFromImprovementCost", () => {
|
||||
it("calculates the correct adventure points for a single rating", () => {
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", 1), 1)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", 12), 1)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", 13), 2)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", 14), 3)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", 15), 4)
|
||||
equal(calculateAdventurePointsFromImprovementCost("D", 1), 4)
|
||||
equal(calculateAdventurePointsFromImprovementCost("D", 12), 4)
|
||||
equal(calculateAdventurePointsFromImprovementCost("D", 13), 8)
|
||||
equal(calculateAdventurePointsFromImprovementCost("D", 14), 12)
|
||||
equal(calculateAdventurePointsFromImprovementCost("D", 15), 16)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", 1), 15)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", 12), 15)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", 13), 15)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", 14), 15)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", 15), 30)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", 16), 45)
|
||||
})
|
||||
|
||||
it("calculates the correct adventure points for a rating range", () => {
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", [1, 1]), 1)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", [1, 12]), 12)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", [1, 13]), 14)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", [1, 14]), 17)
|
||||
equal(calculateAdventurePointsFromImprovementCost("A", [1, 15]), 21)
|
||||
equal(calculateAdventurePointsFromImprovementCost("D", [1, 15]), 84)
|
||||
equal(calculateAdventurePointsFromImprovementCost("E", [1, 16]), 285)
|
||||
})
|
||||
|
||||
it("throws a RangeError for an invalid rating range", () => {
|
||||
throws(() => calculateAdventurePointsFromImprovementCost("A", [13, 12]), {
|
||||
name: "RangeError",
|
||||
message: "Invalid rating range: 13 - 12",
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user