feat: add library entries for close and ranged combattechniques, cantrips, spells, rituals, blessings, liturgical chants and ceremonies

This commit is contained in:
Lukas Obermann
2024-10-04 13:37:04 +02:00
parent 6d0057b7f0
commit e49cc0d90c
37 changed files with 2981 additions and 36 deletions
+15 -13
View File
@@ -18,7 +18,7 @@
"moment": "^2.30.1",
"mousetrap": "1.6.5",
"optolith-character-schema": "^0.0.3",
"optolith-database-schema": "^0.16.8",
"optolith-database-schema": "^0.17.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
@@ -10641,9 +10641,10 @@
"integrity": "sha512-65wA26OjrFjvtCMaAieLZl2TMjYBx63E0nWRrZyWAzT0gjZ/FI8G8X7ouEF2EuTWYM2YYQMblq8/fK+PhGtWug=="
},
"node_modules/optolith-database-schema": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.16.8.tgz",
"integrity": "sha512-1sKJQHtmxYFmlg/fempwdFPsz4bEOfaTpbPV7kl/bIfFaouHgx/ghnb8i/B5EGS7XtnMpj7f/Tonn2W6gIkCsA==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.17.0.tgz",
"integrity": "sha512-pvrJXVvivBbJ8CeFlcCoOYZOCiUwvjkJs77rfLB7coRFrnhI0PdVSGuG1v/XPCDOW3g5RWcKDPKmqSxvBDIIvg==",
"license": "MPL-2.0",
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@@ -15067,9 +15068,10 @@
"dev": true
},
"node_modules/yaml": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
"integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
@@ -22851,9 +22853,9 @@
"integrity": "sha512-65wA26OjrFjvtCMaAieLZl2TMjYBx63E0nWRrZyWAzT0gjZ/FI8G8X7ouEF2EuTWYM2YYQMblq8/fK+PhGtWug=="
},
"optolith-database-schema": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.16.8.tgz",
"integrity": "sha512-1sKJQHtmxYFmlg/fempwdFPsz4bEOfaTpbPV7kl/bIfFaouHgx/ghnb8i/B5EGS7XtnMpj7f/Tonn2W6gIkCsA==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.17.0.tgz",
"integrity": "sha512-pvrJXVvivBbJ8CeFlcCoOYZOCiUwvjkJs77rfLB7coRFrnhI0PdVSGuG1v/XPCDOW3g5RWcKDPKmqSxvBDIIvg==",
"requires": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@@ -26011,9 +26013,9 @@
"dev": true
},
"yaml": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
"integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ=="
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q=="
},
"yargs": {
"version": "17.6.2",
+1 -1
View File
@@ -20,7 +20,7 @@
"moment": "^2.30.1",
"mousetrap": "1.6.5",
"optolith-character-schema": "^0.0.3",
"optolith-database-schema": "^0.16.8",
"optolith-database-schema": "^0.17.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
@@ -3,10 +3,18 @@ import { assertExhaustive } from "../../shared/utils/typeSafety.ts"
import { DisplayableMainIdentifier } from "../slices/inlineWikiSlice.ts"
import "./InlineLibrary.scss"
import { InlineLibraryPlaceholder } from "./InlineLibraryPlaceholder.tsx"
import { InlineLibraryBlessing } from "./entities/InlineLibraryBlessing.tsx"
import { InlineLibraryCantrip } from "./entities/InlineLibraryCantrip.tsx"
import { InlineLibraryCeremony } from "./entities/InlineLibraryCeremony.tsx"
import { InlineLibraryCloseCombatTechnique } from "./entities/InlineLibraryCloseCombatTechnique.tsx"
import { InlineLibraryExperienceLevel } from "./entities/InlineLibraryExperienceLevel.tsx"
import { InlineLibraryFocusRule } from "./entities/InlineLibraryFocusRule.tsx"
import { InlineLibraryLiturgicalChant } from "./entities/InlineLibraryLiturgicalChant.tsx"
import { InlineLibraryOptionalRule } from "./entities/InlineLibraryOptionalRule.tsx"
import { InlineLibraryRangedCombatTechnique } from "./entities/InlineLibraryRangedCombatTechnique.tsx"
import { InlineLibraryRitual } from "./entities/InlineLibraryRitual.tsx"
import { InlineLibrarySkill } from "./entities/InlineLibrarySkill.tsx"
import { InlineLibrarySpell } from "./entities/InlineLibrarySpell.tsx"
type Props = {
id: DisplayableMainIdentifier
@@ -48,7 +56,7 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
case "BlessedTradition":
return <InlineLibraryPlaceholder />
case "Blessing":
return <InlineLibraryPlaceholder />
return <InlineLibraryBlessing id={id.blessing} />
case "Book":
return <InlineLibraryPlaceholder />
case "BowlEnchantment":
@@ -56,7 +64,7 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
case "BrawlingSpecialAbility":
return <InlineLibraryPlaceholder />
case "Cantrip":
return <InlineLibraryPlaceholder />
return <InlineLibraryCantrip id={id.cantrip} />
case "CauldronEnchantment":
return <InlineLibraryPlaceholder />
case "CeremonialItem":
@@ -64,11 +72,11 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
case "CeremonialItemSpecialAbility":
return <InlineLibraryPlaceholder />
case "Ceremony":
return <InlineLibraryPlaceholder />
return <InlineLibraryCeremony id={id.ceremony} />
case "ChronicleEnchantment":
return <InlineLibraryPlaceholder />
case "CloseCombatTechnique":
return <InlineLibraryPlaceholder />
return <InlineLibraryCloseCombatTechnique id={id.close_combat_technique} />
case "Clothes":
return <InlineLibraryPlaceholder />
case "CombatSpecialAbility":
@@ -134,7 +142,7 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
case "Liebesspielzeug":
return <InlineLibraryPlaceholder />
case "LiturgicalChant":
return <InlineLibraryPlaceholder />
return <InlineLibraryLiturgicalChant id={id.liturgical_chant} />
case "LiturgicalStyleSpecialAbility":
return <InlineLibraryPlaceholder />
case "LuxuryGood":
@@ -178,11 +186,11 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
case "Race":
return <InlineLibraryPlaceholder />
case "RangedCombatTechnique":
return <InlineLibraryPlaceholder />
return <InlineLibraryRangedCombatTechnique id={id.ranged_combat_technique} />
case "RingEnchantment":
return <InlineLibraryPlaceholder />
case "Ritual":
return <InlineLibraryPlaceholder />
return <InlineLibraryRitual id={id.ritual} />
case "RopeOrChain":
return <InlineLibraryPlaceholder />
case "Sermon":
@@ -198,7 +206,7 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
case "SkillStyleSpecialAbility":
return <InlineLibraryPlaceholder />
case "Spell":
return <InlineLibraryPlaceholder />
return <InlineLibrarySpell id={id.spell} />
case "SpellSwordEnchantment":
return <InlineLibraryPlaceholder />
case "StaffEnchantment":
@@ -0,0 +1,25 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getBlessingLibraryEntry } from "../../../shared/domain/rated/liturgicalChant.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a blessing.
*/
export const InlineLibraryBlessing: FC<Props> = ({ id }) => {
const getTargetCategoryById = useAppSelector(SelectGetById.Static.TargetCategory)
const entry = useAppSelector(SelectGetById.Static.Blessing)(id)
return (
<LibraryEntry
createLibraryEntry={getBlessingLibraryEntry(entry, {
getTargetCategoryById,
})}
/>
)
}
@@ -0,0 +1,31 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getCantripLibraryEntry } from "../../../shared/domain/rated/spell.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a cantrip.
*/
export const InlineLibraryCantrip: FC<Props> = ({ id }) => {
const getCurriculumById = useAppSelector(SelectGetById.Static.Curriculum)
const getTargetCategoryById = useAppSelector(SelectGetById.Static.TargetCategory)
const getPropertyById = useAppSelector(SelectGetById.Static.Property)
const getMagicalTraditionById = useAppSelector(SelectGetById.Static.MagicalTradition)
const entry = useAppSelector(SelectGetById.Static.Cantrip)(id)
return (
<LibraryEntry
createLibraryEntry={getCantripLibraryEntry(entry, {
getTargetCategoryById,
getPropertyById,
getMagicalTraditionById,
getCurriculumById,
})}
/>
)
}
@@ -0,0 +1,35 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getCeremonyLibraryEntry } from "../../../shared/domain/rated/liturgicalChant.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a ceremony.
*/
export const InlineLibraryCeremony: FC<Props> = ({ id }) => {
const getAttributeById = useAppSelector(SelectGetById.Static.Attribute)
const getDerivedCharacteristicById = useAppSelector(SelectGetById.Static.DerivedCharacteristic)
const getSkillModificationLevelById = useAppSelector(SelectGetById.Static.SkillModificationLevel)
const getTargetCategoryById = useAppSelector(SelectGetById.Static.TargetCategory)
const getBlessedTraditionById = useAppSelector(SelectGetById.Static.BlessedTradition)
const getAspectById = useAppSelector(SelectGetById.Static.Aspect)
const entry = useAppSelector(SelectGetById.Static.Ceremony)(id)
return (
<LibraryEntry
createLibraryEntry={getCeremonyLibraryEntry(entry, {
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getBlessedTraditionById,
getAspectById,
})}
/>
)
}
@@ -0,0 +1,25 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getCloseCombatTechniqueLibraryEntry } from "../../../shared/domain/rated/combatTechnique.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a close combat technique.
*/
export const InlineLibraryCloseCombatTechnique: FC<Props> = ({ id }) => {
const getAttributeById = useAppSelector(SelectGetById.Static.Attribute)
const entry = useAppSelector(SelectGetById.Static.CloseCombatTechnique)(id)
return (
<LibraryEntry
createLibraryEntry={getCloseCombatTechniqueLibraryEntry(entry, {
getAttributeById,
})}
/>
)
}
@@ -0,0 +1,35 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getLiturgicalChantLibraryEntry } from "../../../shared/domain/rated/liturgicalChant.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a liturgical chant.
*/
export const InlineLibraryLiturgicalChant: FC<Props> = ({ id }) => {
const getAttributeById = useAppSelector(SelectGetById.Static.Attribute)
const getDerivedCharacteristicById = useAppSelector(SelectGetById.Static.DerivedCharacteristic)
const getSkillModificationLevelById = useAppSelector(SelectGetById.Static.SkillModificationLevel)
const getTargetCategoryById = useAppSelector(SelectGetById.Static.TargetCategory)
const getBlessedTraditionById = useAppSelector(SelectGetById.Static.BlessedTradition)
const getAspectById = useAppSelector(SelectGetById.Static.Aspect)
const entry = useAppSelector(SelectGetById.Static.LiturgicalChant)(id)
return (
<LibraryEntry
createLibraryEntry={getLiturgicalChantLibraryEntry(entry, {
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getBlessedTraditionById,
getAspectById,
})}
/>
)
}
@@ -0,0 +1,25 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getRangedCombatTechniqueLibraryEntry } from "../../../shared/domain/rated/combatTechnique.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a ranged combat technique.
*/
export const InlineLibraryRangedCombatTechnique: FC<Props> = ({ id }) => {
const getAttributeById = useAppSelector(SelectGetById.Static.Attribute)
const entry = useAppSelector(SelectGetById.Static.RangedCombatTechnique)(id)
return (
<LibraryEntry
createLibraryEntry={getRangedCombatTechniqueLibraryEntry(entry, {
getAttributeById,
})}
/>
)
}
@@ -0,0 +1,35 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getRitualLibraryEntry } from "../../../shared/domain/rated/spell.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a ritual.
*/
export const InlineLibraryRitual: FC<Props> = ({ id }) => {
const getAttributeById = useAppSelector(SelectGetById.Static.Attribute)
const getDerivedCharacteristicById = useAppSelector(SelectGetById.Static.DerivedCharacteristic)
const getSkillModificationLevelById = useAppSelector(SelectGetById.Static.SkillModificationLevel)
const getTargetCategoryById = useAppSelector(SelectGetById.Static.TargetCategory)
const getPropertyById = useAppSelector(SelectGetById.Static.Property)
const getMagicalTraditionById = useAppSelector(SelectGetById.Static.MagicalTradition)
const entry = useAppSelector(SelectGetById.Static.Ritual)(id)
return (
<LibraryEntry
createLibraryEntry={getRitualLibraryEntry(entry, {
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getPropertyById,
getMagicalTraditionById,
})}
/>
)
}
@@ -0,0 +1,35 @@
import { FC } from "react"
import { LibraryEntry } from "../../../shared/components/libraryEntry/LibraryEntry.tsx"
import { getSpellLibraryEntry } from "../../../shared/domain/rated/spell.ts"
import { useAppSelector } from "../../hooks/redux.ts"
import { SelectGetById } from "../../selectors/basicCapabilitySelectors.ts"
type Props = {
id: number
}
/**
* Displays all information about a spell.
*/
export const InlineLibrarySpell: FC<Props> = ({ id }) => {
const getAttributeById = useAppSelector(SelectGetById.Static.Attribute)
const getDerivedCharacteristicById = useAppSelector(SelectGetById.Static.DerivedCharacteristic)
const getSkillModificationLevelById = useAppSelector(SelectGetById.Static.SkillModificationLevel)
const getTargetCategoryById = useAppSelector(SelectGetById.Static.TargetCategory)
const getPropertyById = useAppSelector(SelectGetById.Static.Property)
const getMagicalTraditionById = useAppSelector(SelectGetById.Static.MagicalTradition)
const entry = useAppSelector(SelectGetById.Static.Spell)(id)
return (
<LibraryEntry
createLibraryEntry={getSpellLibraryEntry(entry, {
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getPropertyById,
getMagicalTraditionById,
})}
/>
)
}
@@ -136,7 +136,7 @@ export const LiturgicalChants: FC = () => {
<ListHeader>
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="group">
{translate("liturgicalchants.header.traditions")}
{translate("Traditions")}
{sortOrder === LiturgiesSortOrder.Group ? ` / ${translate("Group")}` : null}
</ListHeaderTag>
<ListHeaderTag className="check">{translate("Check")}</ListHeaderTag>
@@ -227,7 +227,7 @@ export const LiturgicalChants: FC = () => {
<ListHeader>
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="group">
{translate("liturgicalchants.header.traditions")}
{translate("Traditions")}
{sortOrder === LiturgiesSortOrder.Group ? ` / ${translate("Group")}` : null}
</ListHeaderTag>
<ListHeaderTag className="value" hint={translate("Skill Rating")}>
@@ -0,0 +1,167 @@
import {
CastingTime,
CastingTimeDuringLovemaking,
FastCastingTime,
FastSkillNonModifiableCastingTime,
ModifiableCastingTime,
SlowCastingTime,
SlowSkillNonModifiableCastingTime,
} from "optolith-database-schema/types/_ActivatableSkillCastingTime"
import { isNotNullish, mapNullable } from "../../../utils/nullable.ts"
import { Translate } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { GetById } from "../../getTypes.ts"
import { ResponsiveTextSize } from "../responsiveText.ts"
import { MISSING_VALUE } from "../unknown.ts"
import { Entity } from "./entity.ts"
import { ModifiableParameter } from "./modifiableParameter.ts"
import { getTextForNonModifiableSuffix } from "./nonModifiable.ts"
import { Speed } from "./speed.ts"
import { formatTimeSpan } from "./units.ts"
const getTextForModifiableCastingTime = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
},
value: ModifiableCastingTime,
env: {
speed: Speed
responsiveText: ResponsiveTextSize
},
): string =>
mapNullable(
deps.getSkillModificationLevelById(value.initial_modification_level),
({ fast: { casting_time: fastTime }, slow: { casting_time: slowTime } }) => {
switch (env.speed) {
case Speed.Fast:
return formatTimeSpan(deps.translate, env.responsiveText, "Actions", fastTime)
case Speed.Slow:
return formatTimeSpan(deps.translate, env.responsiveText, slowTime.unit, slowTime.value)
default:
return assertExhaustive(env.speed)
}
},
) ?? MISSING_VALUE
const getTextForFastSkillNonModifiableCastingTime = (
deps: {
translate: Translate
},
value: FastSkillNonModifiableCastingTime,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): string =>
formatTimeSpan(deps.translate, env.responsiveText, "Actions", value.actions) +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.CastingTime,
env.responsiveText,
)
const getTextForSlowSkillNonModifiableCastingTime = (
deps: {
translate: Translate
},
value: SlowSkillNonModifiableCastingTime,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): string =>
formatTimeSpan(deps.translate, env.responsiveText, value.unit, value.value) +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.CastingTime,
env.responsiveText,
)
const getTextForCastingTime = <NonModifiable extends object>(
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
},
value: CastingTime<NonModifiable>,
env: {
speed: Speed
responsiveText: ResponsiveTextSize
},
getTextForNonModifiableCastingTime: (value: NonModifiable) => string,
): string => {
switch (value.tag) {
case "Modifiable":
return getTextForModifiableCastingTime(deps, value.modifiable, env)
case "NonModifiable":
return getTextForNonModifiableCastingTime(value.non_modifiable)
default:
return assertExhaustive(value)
}
}
const getTextForCastingTimeDuringLovemaking = (
deps: {
translate: Translate
},
value: CastingTimeDuringLovemaking,
env: {
responsiveText: ResponsiveTextSize
},
): string => formatTimeSpan(deps.translate, env.responsiveText, value.unit, value.value)
/**
* Get the text for the casting time of a fast activatable skill.
*/
export const getTextForFastCastingTime = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
},
value: FastCastingTime,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): string =>
[
mapNullable(value.default, def =>
getTextForCastingTime(deps, def, { ...env, speed: Speed.Fast }, nonModifiableValue =>
getTextForFastSkillNonModifiableCastingTime(deps, nonModifiableValue, env),
),
),
mapNullable(value.during_lovemaking, duringLovemaking =>
getTextForCastingTimeDuringLovemaking(deps, duringLovemaking, env),
),
]
.filter(isNotNullish)
.join(" / ")
/**
* Get the text for the casting time of a slow activatable skill.
*/
export const getTextForSlowCastingTime = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
},
value: SlowCastingTime,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): string =>
[
mapNullable(value.default, def =>
getTextForCastingTime(deps, def, { ...env, speed: Speed.Slow }, nonModifiableValue =>
getTextForSlowSkillNonModifiableCastingTime(deps, nonModifiableValue, env),
),
),
mapNullable(value.during_lovemaking, duringLovemaking =>
getTextForCastingTimeDuringLovemaking(deps, duringLovemaking, env),
),
]
.filter(isNotNullish)
.join(" / ")
@@ -0,0 +1,25 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { translateMock } from "../../../utils/translate.ts"
import { getTextForCheckResultBased } from "./checkResultBased.ts"
describe("getTextForCheckResultBased", () => {
it("should return the value text for a check-result-based parameter of an activatable skill", () => {
assert.equal(getTextForCheckResultBased({ base: "QualityLevels" }, translateMock), "QL")
assert.equal(getTextForCheckResultBased({ base: "SkillPoints" }, translateMock), "SP")
assert.equal(
getTextForCheckResultBased(
{ base: "QualityLevels", modifier: { arithmetic: "Divide", value: 2 } },
translateMock,
),
"QL / 2",
)
assert.equal(
getTextForCheckResultBased(
{ base: "SkillPoints", modifier: { arithmetic: "Multiply", value: 3 } },
translateMock,
),
"SP × 3",
)
})
})
@@ -0,0 +1,42 @@
import {
CheckResultArithmetic,
CheckResultBased,
CheckResultValue,
} from "optolith-database-schema/types/_ActivatableSkillCheckResultBased"
import { mapNullableDefault } from "../../../utils/nullable.ts"
import { Translate } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
const getCheckResultBaseValue = (baseValue: CheckResultValue, translate: Translate) => {
switch (baseValue) {
case "QualityLevels":
return translate("QL")
case "SkillPoints":
return translate("SP")
default:
return assertExhaustive(baseValue)
}
}
const getArithmeticSymbol = (arithmetic: CheckResultArithmetic) => {
switch (arithmetic) {
case "Divide":
return ` / `
case "Multiply":
return ` × `
default:
return assertExhaustive(arithmetic)
}
}
/**
* Returns the value text for a check-result-based parameter of an activatable
* skill.
*/
export const getTextForCheckResultBased = (value: CheckResultBased, translate: Translate): string =>
getCheckResultBaseValue(value.base, translate) +
mapNullableDefault(
value.modifier,
modifier => getArithmeticSymbol(modifier.arithmetic) + modifier.value,
"",
)
@@ -0,0 +1,456 @@
import {
CostMap,
IndefiniteOneTimeCost,
ModifiableOneTimeCost,
MultipleOneTimeCosts,
NonModifiableOneTimeCost,
NonModifiableOneTimeCostPerCountable,
OneTimeCost,
SingleOneTimeCost,
SustainedCost,
} from "optolith-database-schema/types/_ActivatableSkillCost"
import { mapNullable, mapNullableDefault } from "../../../utils/nullable.ts"
import { Translate, TranslateMap } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { GetById } from "../../getTypes.ts"
import {
getResponsiveText,
getResponsiveTextOptional,
replaceTextIfRequested,
responsive,
ResponsiveTextSize,
} from "../responsiveText.ts"
import { MISSING_VALUE } from "../unknown.ts"
import { Entity } from "./entity.ts"
import { ModifiableParameter } from "./modifiableParameter.ts"
import { getTextForNonModifiableSuffix } from "./nonModifiable.ts"
import { getModifiableBySpeed, Speed } from "./speed.ts"
import { formatCost, formatTimeSpan } from "./units.ts"
const getTextForModifiableOneTimeCost = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: ModifiableOneTimeCost,
env: {
speed: Speed
entity: Entity
responsiveText: ResponsiveTextSize
},
): string =>
mapNullable(
deps.getSkillModificationLevelById(value.initial_modification_level),
modificationLevel => {
const cost = getModifiableBySpeed(
modificationLevel,
env.speed,
config => config.cost,
config => config.cost,
)
return replaceTextIfRequested(
value.translations,
formatCost(deps.translate, env.entity, cost),
deps.translateMap,
env.responsiveText,
)
},
) ?? MISSING_VALUE
const getMinimumText = (
isMinimum: boolean | undefined,
translate: Translate,
responsiveText: ResponsiveTextSize,
) =>
isMinimum !== true
? ""
: responsive(
responsiveText,
() => translate("at least "),
() => translate("min. "),
)
const getTextForNonModifiableOneTimeCostPerCountable = (
deps: {
translate: Translate
translateMap: TranslateMap
formatCost: (x: number | string) => string
},
value: NonModifiableOneTimeCostPerCountable | undefined,
env: {
responsiveText: ResponsiveTextSize
},
) =>
mapNullable(value, perCountable => {
const countableText = responsive(
env.responsiveText,
entity => deps.translate(" per {0}", entity),
entity => deps.translate("/{0}", entity),
getResponsiveText(
deps.translateMap(perCountable.translations)?.countable,
env.responsiveText,
),
)
const minimumTotalText =
mapNullable(perCountable.minimum_total, minimumTotal =>
deps.translate(", minimum of {0}", deps.formatCost(minimumTotal)),
) ?? ""
return countableText + minimumTotalText
}) ?? ""
const getTextForPermanentValue = (
value: number | undefined,
responsiveText: ResponsiveTextSize,
translate: Translate,
) =>
value === undefined
? ""
: responsive(
responsiveText,
perm => translate(", {0} of which are permanent", perm),
perm => translate(" ({0} perm.)", perm),
value,
)
const getTextForNonModifiableOneTimeCost = (
deps: { translate: Translate; translateMap: TranslateMap },
value: NonModifiableOneTimeCost,
env: {
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
const isMinimum = getMinimumText(value.is_minimum, deps.translate, env.responsiveText)
const formatCostP = formatCost.bind(this, deps.translate, env.entity)
const per = getTextForNonModifiableOneTimeCostPerCountable(
{ ...deps, formatCost: formatCostP },
value.per,
env,
)
const permanent = getTextForPermanentValue(
value.permanent_value,
env.responsiveText,
deps.translate,
)
const translation = deps.translateMap(value.translations)
const note = mapNullableDefault(
translation === undefined || translation.note === undefined
? undefined
: getResponsiveTextOptional(translation.note, env.responsiveText),
noteIfPresent => ` (${noteIfPresent})`,
"",
)
const cannotModify = getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Cost,
env.responsiveText,
)
return isMinimum + formatCostP(value.value) + per + permanent + note + cannotModify
}
const getTextForIndefiniteOneTimeCost = (
deps: { translate: Translate; translateMap: TranslateMap },
value: IndefiniteOneTimeCost,
env: {
responsiveText: ResponsiveTextSize
entity: Entity
},
): string =>
(getResponsiveText(deps.translateMap(value.translations)?.description, env.responsiveText) ??
"") +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Cost,
env.responsiveText,
)
const getTextForSingleOneTimeCost = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: SingleOneTimeCost,
env: {
speed: Speed
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
switch (value.tag) {
case "Modifiable":
return getTextForModifiableOneTimeCost(deps, value.modifiable, env)
case "NonModifiable":
return getTextForNonModifiableOneTimeCost(deps, value.non_modifiable, env)
case "Indefinite":
return getTextForIndefiniteOneTimeCost(deps, value.indefinite, env)
default:
return assertExhaustive(value)
}
}
const getTextForMultipleOneTimeCosts = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: MultipleOneTimeCosts,
type: "conjunction" | "disjunction",
env: {
speed: Speed
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
const modifiable = !value.every(part => part.tag === "Modifiable")
? getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Cost,
env.responsiveText,
)
: ""
return (
value
.map(part => getTextForSingleOneTimeCost(deps, part, env))
.join(
(() => {
switch (type) {
case "conjunction":
return responsive(
env.responsiveText,
() => deps.translate(" and "),
() => deps.translate(" + "),
)
case "disjunction":
return responsive(
env.responsiveText,
() => deps.translate(" or "),
() => deps.translate(" / "),
)
default:
return assertExhaustive(type)
}
})(),
) + modifiable
)
}
const getTextForCostMap = (
deps: {
translate: Translate
translateMap: TranslateMap
},
value: CostMap,
env: {
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
const translation = deps.translateMap(value.translations)
if (value.translations !== undefined && translation === undefined) {
return MISSING_VALUE
}
if (translation?.replacement !== undefined) {
return translation.replacement
}
const costs = value.options.map(option => option.value).join("/")
const labels = value.options
.map(option => deps.translateMap(option.translations)?.label ?? MISSING_VALUE)
.join("/")
const permanentCosts = value.options.every(option => option.permanent_value !== undefined)
? value.options.map(option => option.permanent_value!).join("/")
: undefined
const formatCostP = formatCost.bind(this, deps.translate, env.entity)
const notModifiable = getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Cost,
env.responsiveText,
)
return (
formatCostP(costs) +
deps.translate(" for ") +
mapNullableDefault(translation?.list_prepend, listPrepend => `${listPrepend} `, "") +
labels +
(translation?.list_append ?? "") +
(permanentCosts !== undefined
? deps.translate(", {0} of which are permanent", formatCostP(permanentCosts))
: "") +
notModifiable
)
}
/**
* Returns the text for the cost of a one-time activatable skill.
*/
export const getTextForOneTimeCost = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: OneTimeCost,
env: {
speed: Speed
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
switch (value.tag) {
case "Single":
return getTextForSingleOneTimeCost(deps, value.single, env)
case "Conjunction":
return getTextForMultipleOneTimeCosts(deps, value.conjunction, "conjunction", env)
case "Disjunction":
return getTextForMultipleOneTimeCosts(deps, value.disjunction, "disjunction", env)
case "Map":
return getTextForCostMap(deps, value.map, env)
default:
return assertExhaustive(value)
}
}
/**
* Returns the text for the cost of a sustained activatable skill.
*/
export const getTextForSustainedCost = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: SustainedCost,
env: {
speed: Speed
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
switch (value.tag) {
case "Modifiable": {
const modificationLevel = deps.getSkillModificationLevelById(
value.modifiable.initial_modification_level,
)
if (modificationLevel === undefined) {
return MISSING_VALUE
}
const cost = (() => {
switch (env.speed) {
case Speed.Fast:
return modificationLevel.fast.cost
case Speed.Slow:
return modificationLevel.slow.cost
default:
return assertExhaustive(env.speed)
}
})()
const formatCostP = formatCost.bind(this, deps.translate, env.entity)
const interval = formatTimeSpan(
deps.translate,
env.responsiveText,
value.modifiable.interval.unit,
value.modifiable.interval.value,
)
return responsive(
env.responsiveText,
() =>
`${formatCostP(cost) + deps.translate(" (casting)")} + ${
formatCostP(cost / 2) + deps.translate(" per {0}", interval)
}`,
() => `${formatCostP(cost)} + ${formatCostP(cost / 2) + deps.translate("/{0}", interval)}`,
)
}
case "NonModifiable": {
const isMinimum = getMinimumText(
value.non_modifiable.is_minimum,
deps.translate,
env.responsiveText,
)
const cost = value.non_modifiable.value
const formatCostP = formatCost.bind(this, deps.translate, env.entity)
const per = (() => {
if (value.non_modifiable.per === undefined) {
return { countable: "", minimumTotal: "" }
}
const countable = responsive(
env.responsiveText,
entity => deps.translate(" per {0}", entity),
entity => deps.translate("/{0}", entity),
getResponsiveText(
deps.translateMap(value.non_modifiable.per.translations)?.countable,
env.responsiveText,
),
)
const minimumTotal =
value.non_modifiable.per.minimum_total !== undefined
? deps.translate(
", minimum of {0}",
formatCostP(value.non_modifiable.per.minimum_total),
)
: ""
return { countable, minimumTotal }
})()
const interval = formatTimeSpan(
deps.translate,
env.responsiveText,
value.non_modifiable.interval.unit,
value.non_modifiable.interval.value,
)
return (
isMinimum +
responsive(
env.responsiveText,
() =>
`${formatCostP(cost) + deps.translate(" (casting)")} + ${
(value.non_modifiable.is_minimum === true
? deps.translate("half of the activation cost")
: formatCostP(cost / 2)) +
per.countable +
deps.translate(" per {0}", interval)
}`,
() =>
`${formatCostP(cost)} + ${
(value.non_modifiable.is_minimum === true ? "50%" : formatCostP(cost / 2)) +
per.countable +
deps.translate("/{0}", interval)
}`,
) +
per.minimumTotal +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Cost,
env.responsiveText,
)
)
}
default:
return assertExhaustive(value)
}
}
@@ -0,0 +1,222 @@
import {
CheckResultBasedDuration,
DurationForOneTime,
DurationForSustained,
FixedDuration,
Immediate,
PermanentDuration,
} from "optolith-database-schema/types/_ActivatableSkillDuration"
import { BlessingDuration } from "optolith-database-schema/types/Blessing"
import { CantripDuration } from "optolith-database-schema/types/Cantrip"
import { mapNullableDefault } from "../../../utils/nullable.ts"
import { Translate, TranslateMap } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import {
ResponsiveTextSize,
getResponsiveText,
replaceTextIfRequested,
responsive,
} from "../responsiveText.ts"
import { getTextForCheckResultBased } from "./checkResultBased.ts"
import { getTextForIsMaximum } from "./isMaximum.ts"
import { formatTimeSpan } from "./units.ts"
const getTextForImmediateDuration = (
deps: {
translate: Translate
translateMap: TranslateMap
},
value: Immediate,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
const text =
deps.translate("Immediate") +
mapNullableDefault(
value.maximum,
max => {
const maxText = formatTimeSpan(deps.translate, env.responsiveText, max.unit, max.value)
return responsive(
env.responsiveText,
() => deps.translate(" (no more than {0})", maxText),
() => deps.translate(" (max. {0})", maxText),
)
},
"",
)
return replaceTextIfRequested(value.translations, text, deps.translateMap, env.responsiveText)
}
const getTextForPermanentDuration = (
deps: {
translate: Translate
translateMap: TranslateMap
},
value: PermanentDuration,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
const translation = deps.translateMap(value.translations)
const text = deps.translate("Permanent")
if (translation?.replacement !== undefined) {
return getResponsiveText(translation.replacement, env.responsiveText).replace("$1", text)
} else {
return text
}
}
const getTextForFixedDuration = (
deps: {
translate: Translate
translateMap: TranslateMap
},
value: FixedDuration,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
const isMaximum = getTextForIsMaximum(value.is_maximum, deps.translate, env.responsiveText)
const unitValue = formatTimeSpan(deps.translate, env.responsiveText, value.unit, value.value)
const text = isMaximum + unitValue
const translation = deps.translateMap(value.translations)
if (translation?.replacement !== undefined) {
return getResponsiveText(translation.replacement, env.responsiveText).replace("$1", text)
} else {
return text
}
}
const getTextForCheckResultBasedDuration = (
deps: {
translate: Translate
translateMap: TranslateMap
},
value: CheckResultBasedDuration,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
const isMaximum = getTextForIsMaximum(value.is_maximum, deps.translate, env.responsiveText)
return formatTimeSpan(
deps.translate,
env.responsiveText,
value.unit,
isMaximum + getTextForCheckResultBased(value, deps.translate),
)
}
/**
* Returns the text for the duration of a one-time activatable skill.
*/
export const getTextForDurationForOneTime = (
deps: {
translate: Translate
translateMap: TranslateMap
},
value: DurationForOneTime,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
switch (value.tag) {
case "Immediate":
return getTextForImmediateDuration(deps, value.immediate, env)
case "Permanent":
return getTextForPermanentDuration(deps, value.permanent, env)
case "Fixed":
return getTextForFixedDuration(deps, value.fixed, env)
case "CheckResultBased":
return getTextForCheckResultBasedDuration(deps, value.check_result_based, env)
case "Indefinite":
return getResponsiveText(
deps.translateMap(value.indefinite.translations)?.description,
env.responsiveText,
)
default:
return assertExhaustive(value)
}
}
/**
* Returns the text for the duration of a sustained activatable skill.
*/
export const getTextForDurationForSustained = (
deps: { translate: Translate },
value: DurationForSustained | undefined,
env: {
responsiveText: ResponsiveTextSize
},
): string =>
value === undefined
? responsive(
env.responsiveText,
() => deps.translate("Sustained"),
() => deps.translate("(S)"),
)
: responsive(
env.responsiveText,
() => deps.translate("no more than "),
() => deps.translate("max. "),
) +
formatTimeSpan(deps.translate, env.responsiveText, value.maximum.unit, value.maximum.value)
/**
* Returns the text for the duration of a cantrip.
*/
export const getTextForCantripDuration = (
deps: { translate: Translate; translateMap: TranslateMap },
value: CantripDuration,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
switch (value.tag) {
case "Immediate":
return getTextForImmediateDuration(deps, value.immediate, env)
case "Fixed":
return getTextForFixedDuration(deps, value.fixed, env)
case "Indefinite":
return getResponsiveText(
deps.translateMap(value.indefinite.translations)?.description,
env.responsiveText,
)
case "DuringLovemaking": {
const { value: lovemakingValue, unit: lovemakingUnit } = value.during_lovemaking
return formatTimeSpan(deps.translate, env.responsiveText, lovemakingUnit, lovemakingValue)
}
default:
return assertExhaustive(value)
}
}
/**
* Returns the text for the duration of a blessing.
*/
export const getTextForBlessingDuration = (
deps: { translate: Translate; translateMap: TranslateMap },
value: BlessingDuration,
env: {
responsiveText: ResponsiveTextSize
},
): string => {
switch (value.tag) {
case "Immediate":
return getTextForImmediateDuration(deps, value.immediate, env)
case "Fixed":
return getTextForFixedDuration(deps, value.fixed, env)
case "Indefinite":
return getResponsiveText(
deps.translateMap(value.indefinite.translations)?.description,
env.responsiveText,
)
default:
return assertExhaustive(value)
}
}
@@ -0,0 +1,59 @@
import { Effect } from "optolith-database-schema/types/_ActivatableSkillEffect"
import { filterNonNullable } from "../../../utils/array.ts"
import { mapNullable } from "../../../utils/nullable.ts"
import { Translate } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { LibraryEntryContent } from "../../libraryEntry.ts"
const getContentPartsForQualityLevels = (
source: {
text_before: string
quality_levels: string[]
text_after?: string
},
getQualityLevelString: (index: number) => string | number,
translate: Translate,
): LibraryEntryContent[] =>
filterNonNullable([
{
label: translate("Effect"),
value: source.text_before,
},
...source.quality_levels.map((text, index) => ({
value: text,
label: translate("QL {0}", getQualityLevelString(index)),
})),
mapNullable(source.text_after, textAfter => ({
value: textAfter,
className: "effect-after",
})),
])
/**
* Gets the text for the effect of an activatable skill.
*/
export const getTextForEffect = (effect: Effect, translate: Translate): LibraryEntryContent[] => {
switch (effect.tag) {
case "Plain":
return [
{
label: translate("Effect"),
value: effect.plain.text,
},
]
case "ForEachQualityLevel":
return getContentPartsForQualityLevels(
effect.for_each_quality_level,
index => index + 1,
translate,
)
case "ForEachTwoQualityLevels":
return getContentPartsForQualityLevels(
effect.for_each_two_quality_levels,
index => `${index * 2 + 1}${index * 2 + 2}`,
translate,
)
default:
return assertExhaustive(effect)
}
}
@@ -0,0 +1,11 @@
/**
* The entity type of an activatable skill.
*/
export enum Entity {
Cantrip,
Spell,
Ritual,
Blessing,
LiturgicalChant,
Ceremony,
}
@@ -0,0 +1,21 @@
import { Translate } from "../../../utils/translate.ts"
import { ResponsiveTextSize, responsive } from "../responsiveText.ts"
/**
* Returns the text to prepend for the `is_maximum` property.
*/
export const getTextForIsMaximum = (
is_maximum: boolean | undefined,
translate: Translate,
responsiveText: ResponsiveTextSize,
): string => {
if (is_maximum !== true) {
return ""
}
return responsive(
responsiveText,
() => translate("no more than "),
() => translate("max. "),
)
}
@@ -0,0 +1,8 @@
/**
* A parameter that is designed to be modifiable.
*/
export enum ModifiableParameter {
CastingTime,
Cost,
Range,
}
@@ -0,0 +1,83 @@
import { Translate } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { ResponsiveTextSize } from "../responsiveText.ts"
import { Entity } from "./entity.ts"
import { ModifiableParameter } from "./modifiableParameter.ts"
/**
* Returns the suffix for the text of a non-modifiable parameter that indicates
* that the parameter cannot be modified.
*/
export const getTextForNonModifiableSuffix = (
translate: Translate,
entity: Entity,
param: ModifiableParameter,
responsiveText: ResponsiveTextSize,
): string => {
if (responsiveText === ResponsiveTextSize.Compressed) {
switch (entity) {
case Entity.Spell:
case Entity.Ritual:
case Entity.LiturgicalChant:
case Entity.Ceremony:
return translate(" (cannot modify)")
case Entity.Cantrip:
case Entity.Blessing:
return ""
default:
return assertExhaustive(entity)
}
}
switch (entity) {
case Entity.Spell:
switch (param) {
case ModifiableParameter.CastingTime:
return translate(" (you cannot use a modification on this spells casting time)")
case ModifiableParameter.Cost:
return translate(" (you cannot use a modification on this spells cost)")
case ModifiableParameter.Range:
return translate(" (you cannot use a modification on this spells range)")
default:
return assertExhaustive(param)
}
case Entity.Ritual:
switch (param) {
case ModifiableParameter.CastingTime:
return translate(" (you cannot use a modification on this rituals ritual time)")
case ModifiableParameter.Cost:
return translate(" (you cannot use a modification on this rituals cost)")
case ModifiableParameter.Range:
return translate(" (you cannot use a modification on this rituals range)")
default:
return assertExhaustive(param)
}
case Entity.LiturgicalChant:
switch (param) {
case ModifiableParameter.CastingTime:
return translate(" (you cannot use a modification on this chants liturgical time)")
case ModifiableParameter.Cost:
return translate(" (you cannot use a modification on this chants cost)")
case ModifiableParameter.Range:
return translate(" (you cannot use a modification on this chants range)")
default:
return assertExhaustive(param)
}
case Entity.Ceremony:
switch (param) {
case ModifiableParameter.CastingTime:
return translate(" (you cannot use a modification on this ceremonys ceremonial time)")
case ModifiableParameter.Cost:
return translate(" (you cannot use a modification on this ceremonys cost)")
case ModifiableParameter.Range:
return translate(" (you cannot use a modification on this ceremonys range)")
default:
return assertExhaustive(param)
}
case Entity.Cantrip:
case Entity.Blessing:
return ""
default:
return assertExhaustive(entity)
}
}
@@ -0,0 +1,13 @@
/**
* Wraps a string in parentheses with a leading space if it is not empty or
* `undefined`.
*/
export const parensIf = (text: string | undefined): string =>
text === undefined || text === "" ? "" : ` (${text})`
/**
* Appends a string in parentheses with a leading space if it is not empty or
* `undefined`.
*/
export const appendInParens = (text: string, append: string | undefined): string =>
append === undefined || append === "" ? text : `${text} (${append})`
@@ -0,0 +1,221 @@
import { Range, RangeUnit } from "optolith-database-schema/types/_ActivatableSkillRange"
import { BlessingRange } from "optolith-database-schema/types/Blessing"
import { CantripRange } from "optolith-database-schema/types/Cantrip"
import { Translate, TranslateMap } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { GetById } from "../../getTypes.ts"
import {
getResponsiveText,
getResponsiveTextOptional,
responsive,
ResponsiveTextSize,
} from "../responsiveText.ts"
import { MISSING_VALUE } from "../unknown.ts"
import { getTextForCheckResultBased } from "./checkResultBased.ts"
import { Entity } from "./entity.ts"
import { getTextForIsMaximum } from "./isMaximum.ts"
import { ModifiableParameter } from "./modifiableParameter.ts"
import { getTextForNonModifiableSuffix } from "./nonModifiable.ts"
import { Speed } from "./speed.ts"
const toRangeUnit = (
unit: RangeUnit,
value: number | string,
translate: Translate,
responsiveText: ResponsiveTextSize,
) => {
switch (unit) {
case "Steps":
return responsive(
responsiveText,
() => translate("{0} yards", value),
() => translate("{0} yd", value),
)
case "Miles":
return responsive(
responsiveText,
() => translate("{0} miles", value),
() => translate("{0} mi.", value),
)
default:
return assertExhaustive(unit)
}
}
/**
* Returns the text for the range of an activatable skill.
*/
export const getTextForActivatableSkillRange = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: Range,
env: {
speed: Speed
responsiveText: ResponsiveTextSize
entity: Entity
},
): string => {
const translation = deps.translateMap(value.translations)
if (value.translations !== undefined && translation === undefined) {
return MISSING_VALUE
}
const rangeValue = (() => {
switch (value.value.tag) {
case "Modifiable": {
const modificationLevel = deps.getSkillModificationLevelById(
value.value.modifiable.initial_modification_level,
)
if (modificationLevel === undefined) {
return MISSING_VALUE
}
const range = (() => {
switch (env.speed) {
case Speed.Fast:
return modificationLevel.fast.range
case Speed.Slow:
return modificationLevel.slow.range
default:
return assertExhaustive(env.speed)
}
})()
if (range === 1) {
return deps.translate("Touch")
}
return toRangeUnit("Steps", range, deps.translate, env.responsiveText)
}
case "Sight":
return deps.translate("Sight")
case "Self":
return deps.translate("Self")
case "Global":
return deps.translate("Global")
case "Touch":
return (
deps.translate("Touch") +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Range,
env.responsiveText,
)
)
case "Fixed": {
return (
toRangeUnit(
value.value.fixed.unit,
value.value.fixed.value,
deps.translate,
env.responsiveText,
) +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Range,
env.responsiveText,
)
)
}
case "CheckResultBased": {
const isMaximum = getTextForIsMaximum(
value.value.check_result_based.is_maximum,
deps.translate,
env.responsiveText,
)
const isRadius =
value.value.check_result_based.is_radius === true ? ` ${deps.translate("Radius")}` : ""
return (
isMaximum +
toRangeUnit(
value.value.check_result_based.unit,
getTextForCheckResultBased(value.value.check_result_based, deps.translate),
deps.translate,
env.responsiveText,
) +
isRadius +
getTextForNonModifiableSuffix(
deps.translate,
env.entity,
ModifiableParameter.Range,
env.responsiveText,
)
)
}
default:
return assertExhaustive(value.value)
}
})()
const withReplacement =
translation?.replacement !== undefined
? getResponsiveText(translation.replacement, env.responsiveText).replace("$1", rangeValue)
: rangeValue
const withNote = (() => {
if (translation?.note === undefined) {
return withReplacement
}
const note = getResponsiveTextOptional(translation.note, env.responsiveText)
if (note === undefined) {
return withReplacement
}
return `${withReplacement} (${note})`
})()
return withNote
}
/**
* Returns the text for the range of a cantrip.
*/
export const getTextForCantripRange = (
deps: { translate: Translate },
value: CantripRange,
env: { responsiveText: ResponsiveTextSize },
): string => {
switch (value.tag) {
case "Self":
return deps.translate("Self")
case "Touch":
return deps.translate("Touch")
case "Fixed": {
return toRangeUnit(value.fixed.unit, value.fixed.value, deps.translate, env.responsiveText)
}
default:
return assertExhaustive(value)
}
}
/**
* Returns the text for the range of a blessing.
*/
export const getTextForBlessingRange = (
deps: { translate: Translate },
value: BlessingRange,
env: { responsiveText: ResponsiveTextSize },
): string => {
switch (value.tag) {
case "Self":
return deps.translate("Self")
case "Touch":
return deps.translate("Touch")
case "Fixed": {
return toRangeUnit(value.fixed.unit, value.fixed.value, deps.translate, env.responsiveText)
}
default:
return assertExhaustive(value)
}
}
@@ -0,0 +1,33 @@
import {
FastSkillModificationLevelConfig,
SkillModificationLevel,
SlowSkillModificationLevelConfig,
} from "optolith-database-schema/types/SkillModificationLevel"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
/**
* The speed of an activatable skill.
*/
export enum Speed {
Fast,
Slow,
}
/**
* Returns a common value for a skill modification level depending on the speed.
*/
export const getModifiableBySpeed = <T>(
level: SkillModificationLevel,
speed: Speed,
fast: (config: FastSkillModificationLevelConfig) => T,
slow: (config: SlowSkillModificationLevelConfig) => T,
): T => {
switch (speed) {
case Speed.Fast:
return fast(level.fast)
case Speed.Slow:
return slow(level.slow)
default:
return assertExhaustive(speed)
}
}
@@ -0,0 +1,55 @@
import { TargetCategory } from "optolith-database-schema/types/_ActivatableSkillTargetCategory"
import { mapNullable } from "../../../utils/nullable.ts"
import { Translate, TranslateMap } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { GetById } from "../../getTypes.ts"
import { LibraryEntryContent } from "../../libraryEntry.ts"
import { MISSING_VALUE } from "../unknown.ts"
import { appendInParens } from "./parensIf.ts"
/**
* Get the text for the target category.
*/
export const getTextForTargetCategory = (
deps: {
translate: Translate
translateMap: TranslateMap
getTargetCategoryById: GetById.Static.TargetCategory
},
values: TargetCategory,
): LibraryEntryContent => ({
label: deps.translate("Target Category"),
value:
values.length === 0
? deps.translate("all")
: values
.map(({ id, translations }) => {
const mainName = (() => {
switch (id.tag) {
case "Self":
return deps.translate("Self")
case "Zone":
return deps.translate("Zone")
case "LiturgicalChantsAndCeremonies":
return deps.translate("Liturgical Chants and Ceremonies")
case "Cantrips":
return deps.translate("Cantrips")
case "Predefined": {
const numericId = id.predefined.id.target_category
const specificTargetCategory = deps.getTargetCategoryById(numericId)
return (
mapNullable(
deps.translateMap(specificTargetCategory?.translations),
translation => translation.name,
) ?? MISSING_VALUE
)
}
default:
return assertExhaustive(id)
}
})()
return appendInParens(mainName, deps.translateMap(translations)?.note)
})
.join(", "),
})
@@ -0,0 +1,123 @@
import { Translate } from "../../../utils/translate.ts"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { responsive, ResponsiveTextSize } from "../responsiveText.ts"
import { Entity } from "./entity.ts"
type TimeSpanUnit =
| "Seconds"
| "Minutes"
| "Hours"
| "Days"
| "Weeks"
| "Months"
| "Years"
| "Centuries"
| "Actions"
| "CombatRounds"
| "SeductionActions"
| "Rounds"
/**
* Returns the text for a time span unit.
*/
export const formatTimeSpan = (
translate: Translate,
responsiveTextSize: ResponsiveTextSize,
unit: TimeSpanUnit,
value: number | string,
): string => {
switch (unit) {
case "Seconds":
return responsive(
responsiveTextSize,
() => translate("{0} seconds", value),
() => translate("{0} s", value),
)
case "Minutes":
return responsive(
responsiveTextSize,
() => translate("{0} minutes", value),
() => translate("{0} min", value),
)
case "Hours":
return responsive(
responsiveTextSize,
() => translate("{0} hours", value),
() => translate("{0} h", value),
)
case "Days":
return responsive(
responsiveTextSize,
() => translate("{0} days", value),
() => translate("{0} d", value),
)
case "Weeks":
return responsive(
responsiveTextSize,
() => translate("{0} weeks", value),
() => translate("{0} wks.", value),
)
case "Months":
return responsive(
responsiveTextSize,
() => translate("{0} months", value),
() => translate("{0} mos.", value),
)
case "Years":
return responsive(
responsiveTextSize,
() => translate("{0} years", value),
() => translate("{0} yrs.", value),
)
case "Centuries":
return responsive(
responsiveTextSize,
() => translate("{0} centuries", value),
() => translate("{0} cent.", value),
)
case "Actions":
return responsive(
responsiveTextSize,
() => translate("{0} actions", value),
() => translate("{0} act", value),
)
case "CombatRounds":
return responsive(
responsiveTextSize,
() => translate("{0} combat rounds", value),
() => translate("{0} CR", value),
)
case "SeductionActions":
return responsive(
responsiveTextSize,
() => translate("{0} seduction actions", value),
() => translate("{0} SA", value),
)
case "Rounds":
return responsive(
responsiveTextSize,
() => translate("{0} rounds", value),
() => translate("{0} rnds", value),
)
default:
return assertExhaustive(unit)
}
}
/**
* Returns the text for a cost unit that is based on the entity type.
*/
export const formatCost = (translate: Translate, entity: Entity, value: number | string) => {
switch (entity) {
case Entity.Cantrip:
case Entity.Spell:
case Entity.Ritual:
return translate("{0} AE", value)
case Entity.Blessing:
case Entity.LiturgicalChant:
case Entity.Ceremony:
return translate("{0} KP", value)
default:
return assertExhaustive(entity)
}
}
@@ -0,0 +1,89 @@
import { LocaleMap } from "optolith-database-schema/types/_LocaleMap"
import {
ResponsiveText,
ResponsiveTextOptional,
ResponsiveTextReplace,
} from "optolith-database-schema/types/_ResponsiveText"
import { mapNullable } from "../../utils/nullable.ts"
import { TranslateMap } from "../../utils/translate.ts"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { MISSING_VALUE } from "./unknown.ts"
/**
* Whether the entry is displayed in a normal or compressed setting. Normal/full
* usually means a full library entry display, whether compressed usually means
* the character sheet.
*/
export enum ResponsiveTextSize {
Compressed,
Full,
}
/**
* Executes one of two functions depending on the responsive text size.
*/
export const responsive = <T, A extends unknown[]>(
size: ResponsiveTextSize,
full: (...args: A) => T,
compressed: (...args: A) => T,
...args: A
): T => {
switch (size) {
case ResponsiveTextSize.Compressed:
return compressed(...args)
case ResponsiveTextSize.Full:
return full(...args)
default:
return assertExhaustive(size)
}
}
/**
* Returns the responsive text for a given size.
*/
export const getResponsiveText = (
value: ResponsiveText | undefined,
size: ResponsiveTextSize,
): string => {
if (value === undefined) {
return MISSING_VALUE
}
return responsive(
size,
() => value.full,
() => value.compressed,
)
}
/**
* Returns the responsive text for a given size if it is defined.
*/
export const getResponsiveTextOptional = (
value: ResponsiveTextOptional | undefined,
size: ResponsiveTextSize,
): string | undefined => {
if (value === undefined) {
return MISSING_VALUE
}
return responsive(
size,
() => value.full,
() => value.compressed,
)
}
/**
* Replaces a text with a given value if a replacement is requested, otherwise
* just return the .
*/
export const replaceTextIfRequested = (
translation: LocaleMap<{ replacement?: ResponsiveTextReplace }> | undefined,
valueToReplace: string,
translateMap: TranslateMap,
responsiveText: ResponsiveTextSize,
) =>
mapNullable(translateMap(translation)?.replacement, replacement =>
getResponsiveText(replacement, responsiveText).replace("$1", valueToReplace),
) ?? valueToReplace
@@ -0,0 +1,4 @@
/**
* String to display when a translation that should be present is missing.
*/
export const MISSING_VALUE = "???"
+128
View File
@@ -0,0 +1,128 @@
import {
FastOneTimePerformanceParameters,
FastSustainedPerformanceParameters,
SlowOneTimePerformanceParameters,
SlowSustainedPerformanceParameters,
} from "optolith-database-schema/types/_ActivatableSkill"
import { Translate, TranslateMap } from "../../utils/translate.ts"
import { GetById } from "../getTypes.ts"
import {
getTextForFastCastingTime,
getTextForSlowCastingTime,
} from "../libraryEntry/activatableSkill/castingTime.ts"
import {
getTextForOneTimeCost,
getTextForSustainedCost,
} from "../libraryEntry/activatableSkill/cost.ts"
import {
getTextForDurationForOneTime,
getTextForDurationForSustained,
} from "../libraryEntry/activatableSkill/duration.ts"
import { Entity } from "../libraryEntry/activatableSkill/entity.ts"
import { getTextForActivatableSkillRange } from "../libraryEntry/activatableSkill/range.ts"
import { Speed } from "../libraryEntry/activatableSkill/speed.ts"
import { ResponsiveTextSize } from "../libraryEntry/responsiveText.ts"
/**
* Get the texts for all fast one-time performance parameters.
*/
export const getTextForFastOneTimePerformanceParameters = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: FastOneTimePerformanceParameters,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getTextForFastCastingTime(deps, value.casting_time, env),
cost: getTextForOneTimeCost(deps, value.cost, { speed: Speed.Fast, ...env }),
range: getTextForActivatableSkillRange(deps, value.range, { speed: Speed.Fast, ...env }),
duration: getTextForDurationForOneTime(deps, value.duration, env),
})
/**
* Get the texts for all fast sustained performance parameters.
*/
export const getTextForFastSustainedPerformanceParameters = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: FastSustainedPerformanceParameters,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getTextForFastCastingTime(deps, value.casting_time, env),
cost: getTextForSustainedCost(deps, value.cost, { speed: Speed.Fast, ...env }),
range: getTextForActivatableSkillRange(deps, value.range, { speed: Speed.Fast, ...env }),
duration: getTextForDurationForSustained(deps, value.duration, env),
})
/**
* Get the texts for all slow one-time performance parameters.
*/
export const getTextForSlowOneTimePerformanceParameters = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: SlowOneTimePerformanceParameters,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getTextForSlowCastingTime(deps, value.casting_time, env),
cost: getTextForOneTimeCost(deps, value.cost, { speed: Speed.Slow, ...env }),
range: getTextForActivatableSkillRange(deps, value.range, { speed: Speed.Slow, ...env }),
duration: getTextForDurationForOneTime(deps, value.duration, env),
})
/**
* Get the texts for all slow sustained performance parameters.
*/
export const getTextForSlowSustainedPerformanceParameters = (
deps: {
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
translate: Translate
translateMap: TranslateMap
},
value: SlowSustainedPerformanceParameters,
env: {
entity: Entity
responsiveText: ResponsiveTextSize
},
): {
castingTime: string
cost: string
range: string
duration: string
} => ({
castingTime: getTextForSlowCastingTime(deps, value.casting_time, env),
cost: getTextForSustainedCost(deps, value.cost, { speed: Speed.Slow, ...env }),
range: getTextForActivatableSkillRange(deps, value.range, { speed: Speed.Slow, ...env }),
duration: getTextForDurationForSustained(deps, value.duration, env),
})
@@ -2,8 +2,12 @@ import { CloseCombatTechnique } from "optolith-database-schema/types/CombatTechn
import { RangedCombatTechnique } from "optolith-database-schema/types/CombatTechnique_Ranged"
import { CombatTechniqueIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences"
import { mapNullable } from "../../utils/nullable.ts"
import { Activatable, countOptions } from "../activatable/activatableEntry.ts"
import { createImprovementCost } from "../adventurePoints/improvementCost.ts"
import { GetById } from "../getTypes.ts"
import { AttributeIdentifier } from "../identifier.ts"
import { createLibraryEntryCreator } from "../libraryEntry.ts"
import { getAttributeValue } from "./attribute.ts"
import { Rated } from "./ratedEntry.ts"
@@ -153,3 +157,73 @@ export const getHighestRequiredAttributeForCombatTechnique = (
value: dynamicCombatTechnique.value - 2 - exceptionalCombatTechniqueBonus,
}
}
/**
* Get a JSON representation of the rules text for a close combat technique.
*/
export const getCloseCombatTechniqueLibraryEntry = createLibraryEntryCreator<
CloseCombatTechnique,
{
getAttributeById: GetById.Static.Attribute
}
>((entry, { getAttributeById }) => ({ translate, translateMap }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
return {
title: translation.name,
className: "combat-technique close-combat-technique",
content: [
mapNullable(translation.special, value => ({
label: translate("Special"),
value,
})),
{
label: translate("Primary Attribute"),
value: entry.primary_attribute
.map(attr => translateMap(getAttributeById(attr.id.attribute)?.translations)?.name)
.join("/"),
},
createImprovementCost(translate, entry.improvement_cost),
],
src: entry.src,
}
})
/**
* Get a JSON representation of the rules text for a ranged combat technique.
*/
export const getRangedCombatTechniqueLibraryEntry = createLibraryEntryCreator<
RangedCombatTechnique,
{
getAttributeById: GetById.Static.Attribute
}
>((entry, { getAttributeById }) => ({ translate, translateMap }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
return {
title: translation.name,
className: "combat-technique ranged-combat-technique",
content: [
mapNullable(translation.special, value => ({
label: translate("Special"),
value,
})),
{
label: translate("Primary Attribute"),
value: entry.primary_attribute
.map(attr => translateMap(getAttributeById(attr.id.attribute)?.translations)?.name)
.join("/"),
},
createImprovementCost(translate, entry.improvement_cost),
],
src: entry.src,
}
})
+367 -3
View File
@@ -1,20 +1,41 @@
import { Aspect } from "optolith-database-schema/types/Aspect"
import { Blessing } from "optolith-database-schema/types/Blessing"
import { Ceremony } from "optolith-database-schema/types/Ceremony"
import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel"
import { LiturgicalChant } from "optolith-database-schema/types/LiturgicalChant"
import { SkillTradition } from "optolith-database-schema/types/_Blessed"
import { LiturgyIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { ImprovementCost } from "optolith-database-schema/types/_ImprovementCost"
import { AspectReference } from "optolith-database-schema/types/_SimpleReferences"
import { SkillCheck } from "optolith-database-schema/types/_SkillCheck"
import { BlessedTradition } from "optolith-database-schema/types/specialAbility/BlessedTradition"
import { count, countByMany } from "../../utils/array.ts"
import { Compare, compareAt, compareNullish, numAsc, reduceCompare } from "../../utils/compare.ts"
import { isNotNullish } from "../../utils/nullable.ts"
import { TranslateMap } from "../../utils/translate.ts"
import { Translate, TranslateMap } from "../../utils/translate.ts"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { Activatable, countOptions } from "../activatable/activatableEntry.ts"
import { compareImprovementCost, fromRaw } from "../adventurePoints/improvementCost.ts"
import { All } from "../getTypes.ts"
import {
compareImprovementCost,
createImprovementCost,
fromRaw,
} from "../adventurePoints/improvementCost.ts"
import { All, GetById } from "../getTypes.ts"
import { AspectIdentifier, createIdentifierObject } from "../identifier.ts"
import { createLibraryEntryCreator, LibraryEntryContent } from "../libraryEntry.ts"
import { getTextForBlessingDuration } from "../libraryEntry/activatableSkill/duration.ts"
import { getTextForEffect } from "../libraryEntry/activatableSkill/effect.ts"
import { Entity } from "../libraryEntry/activatableSkill/entity.ts"
import { getTextForBlessingRange } from "../libraryEntry/activatableSkill/range.ts"
import { getTextForTargetCategory } from "../libraryEntry/activatableSkill/targetCategory.ts"
import { ResponsiveTextSize } from "../libraryEntry/responsiveText.ts"
import { LiturgiesSortOrder } from "../sortOrders.ts"
import {
getTextForFastOneTimePerformanceParameters,
getTextForFastSustainedPerformanceParameters,
getTextForSlowOneTimePerformanceParameters,
getTextForSlowSustainedPerformanceParameters,
} from "./activatableSkill.ts"
import { DisplayedActiveLiturgy } from "./liturgicalChantActive.ts"
import { DisplayedInactiveLiturgy } from "./liturgicalChantInactive.ts"
import {
@@ -26,6 +47,7 @@ import {
isRatedActive,
isRatedWithEnhancementsActive,
} from "./ratedEntry.ts"
import { getTextForCheck } from "./skillCheck.ts"
/**
* Returns the value for a dynamic liturgical chant entry that might not exist
@@ -533,3 +555,345 @@ export const filterAndSortDisplayed = <T extends DisplayedActiveLiturgy | Displa
// maybe (aspects_str)
// (pipe (NumIdName.A.name, gr_str => `${aspects_str} / ${gr_str}`))
// )
const getTextForTraditions = (
deps: {
translate: Translate
translateMap: TranslateMap
localeCompare: Compare<string>
getBlessedTraditionById: GetById.Static.BlessedTradition
getAspectById: GetById.Static.Aspect
},
values: SkillTradition[],
): LibraryEntryContent => {
const getAspectName = (ref: AspectReference) =>
deps.translateMap(deps.getAspectById(ref.id.aspect)?.translations)?.name
const text = values
.map(trad => {
switch (trad.tag) {
case "GeneralAspect":
return getAspectName(trad.general_aspect)
case "Tradition": {
const traditionTranslation = deps.translateMap(
deps.getBlessedTraditionById(trad.tradition.tradition.id.blessed_tradition)
?.translations,
)
const name = traditionTranslation?.name_compressed ?? traditionTranslation?.name
if (name === undefined) {
return undefined
}
const aspects =
trad.tradition.aspects
?.map(getAspectName)
.filter(isNotNullish)
.sort(deps.localeCompare) ?? []
if (aspects.length === 0) {
return name
}
return `${name} (${aspects.join(" and ")})`
}
default:
return assertExhaustive(trad)
}
})
.filter(isNotNullish)
.join(", ")
return {
label: deps.translate("Traditions"),
value: text,
}
}
/**
* Get a JSON representation of the rules text for a blessing.
*/
export const getBlessingLibraryEntry = createLibraryEntryCreator<
Blessing,
{
getTargetCategoryById: GetById.Static.TargetCategory
}
>((entry, { getTargetCategoryById }) => ({ translate, translateMap }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
const range = getTextForBlessingRange({ translate }, entry.parameters.range, {
responsiveText: ResponsiveTextSize.Full,
})
const duration = getTextForBlessingDuration(
{ translate, translateMap },
entry.parameters.duration,
{
responsiveText: ResponsiveTextSize.Full,
},
)
return {
title: translation.name,
className: "blessing",
content: [
{
label: translate("Effect"),
value: translation.effect,
},
{
label: translate("Range"),
value: range !== translation.range ? `***${range}*** (${translation.range})` : range,
},
{
label: translate("Duration"),
value:
duration !== translation.duration
? `***${duration}*** (${translation.duration})`
: duration,
},
getTextForTargetCategory({ translate, translateMap, getTargetCategoryById }, entry.target),
],
src: entry.src,
}
})
/**
* Get a JSON representation of the rules text for a liturgical chant.
*/
export const getLiturgicalChantLibraryEntry = createLibraryEntryCreator<
LiturgicalChant,
{
getAttributeById: GetById.Static.Attribute
getDerivedCharacteristicById: GetById.Static.DerivedCharacteristic
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
getTargetCategoryById: GetById.Static.TargetCategory
getBlessedTraditionById: GetById.Static.BlessedTradition
getAspectById: GetById.Static.Aspect
}
>(
(
entry,
{
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getBlessedTraditionById,
getAspectById,
},
) =>
({ translate, translateMap, localeCompare }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
const { castingTime, cost, range, duration } = (() => {
switch (entry.parameters.tag) {
case "OneTime":
return getTextForFastOneTimePerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.one_time,
{
entity: Entity.LiturgicalChant,
responsiveText: ResponsiveTextSize.Full,
},
)
case "Sustained":
return getTextForFastSustainedPerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.sustained,
{
entity: Entity.LiturgicalChant,
responsiveText: ResponsiveTextSize.Full,
},
)
default:
return assertExhaustive(entry.parameters)
}
})()
return {
title: translation.name,
className: "liturgical-chant",
content: [
getTextForCheck({ translate, translateMap, getAttributeById }, entry.check, {
value: entry.check_penalty,
responsiveText: ResponsiveTextSize.Full,
getDerivedCharacteristicById,
}),
...getTextForEffect(translation.effect, translate),
{
label: translate("Liturgical Time"),
value:
castingTime !== translation.casting_time.full
? `***${castingTime}*** (${translation.casting_time.full})`
: castingTime,
},
{
label: translate("KP Cost"),
value:
cost !== translation.cost.full ? `***${cost}*** (${translation.cost.full})` : cost,
},
{
label: translate("Range"),
value:
range !== translation.range.full
? `***${range}*** (${translation.range.full})`
: range,
},
{
label: translate("Duration"),
value:
duration !== translation.duration.full
? `***${duration}*** (${translation.duration.full})`
: duration,
},
getTextForTargetCategory(
{ translate, translateMap, getTargetCategoryById },
entry.target,
),
getTextForTraditions(
{ translate, translateMap, localeCompare, getBlessedTraditionById, getAspectById },
entry.traditions,
),
createImprovementCost(translate, entry.improvement_cost),
],
src: entry.src,
}
},
)
/**
* Get a JSON representation of the rules text for a ceremony.
*/
export const getCeremonyLibraryEntry = createLibraryEntryCreator<
Ceremony,
{
getAttributeById: GetById.Static.Attribute
getDerivedCharacteristicById: GetById.Static.DerivedCharacteristic
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
getTargetCategoryById: GetById.Static.TargetCategory
getBlessedTraditionById: GetById.Static.BlessedTradition
getAspectById: GetById.Static.Aspect
}
>(
(
entry,
{
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getBlessedTraditionById,
getAspectById,
},
) =>
({ translate, translateMap, localeCompare }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
const { castingTime, cost, range, duration } = (() => {
switch (entry.parameters.tag) {
case "OneTime":
return getTextForSlowOneTimePerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.one_time,
{
entity: Entity.Ceremony,
responsiveText: ResponsiveTextSize.Full,
},
)
case "Sustained":
return getTextForSlowSustainedPerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.sustained,
{
entity: Entity.Ceremony,
responsiveText: ResponsiveTextSize.Full,
},
)
default:
return assertExhaustive(entry.parameters)
}
})()
return {
title: translation.name,
className: "ceremony",
content: [
getTextForCheck({ translate, translateMap, getAttributeById }, entry.check, {
value: entry.check_penalty,
responsiveText: ResponsiveTextSize.Full,
getDerivedCharacteristicById,
}),
...getTextForEffect(translation.effect, translate),
{
label: translate("Ceremonial Time"),
value:
castingTime !== translation.casting_time.full
? `***${castingTime}*** (${translation.casting_time.full})`
: castingTime,
},
{
label: translate("KP Cost"),
value:
cost !== translation.cost.full ? `***${cost}*** (${translation.cost.full})` : cost,
},
{
label: translate("Range"),
value:
range !== translation.range.full
? `***${range}*** (${translation.range.full})`
: range,
},
{
label: translate("Duration"),
value:
duration !== translation.duration.full
? `***${duration}*** (${translation.duration.full})`
: duration,
},
getTextForTargetCategory(
{ translate, translateMap, getTargetCategoryById },
entry.target,
),
getTextForTraditions(
{ translate, translateMap, localeCompare, getBlessedTraditionById, getAspectById },
entry.traditions,
),
createImprovementCost(translate, entry.improvement_cost),
],
src: entry.src,
}
},
)
+442 -2
View File
@@ -9,18 +9,23 @@ import {
SpellworkIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import { ImprovementCost as RawImprovementCost } from "optolith-database-schema/types/_ImprovementCost"
import {
MagicalTraditionReference,
PropertyReference,
} from "optolith-database-schema/types/_SimpleReferences"
import { SkillCheck } from "optolith-database-schema/types/_SkillCheck"
import { Traditions } from "optolith-database-schema/types/_Spellwork"
import { count, countBy, sum } from "../../utils/array.ts"
import { Compare, compareAt, compareNullish, numAsc, reduceCompare } from "../../utils/compare.ts"
import { isNotNullish, mapNullableDefault } from "../../utils/nullable.ts"
import { TranslateMap } from "../../utils/translate.ts"
import { isNotNullish, mapNullable, mapNullableDefault } from "../../utils/nullable.ts"
import { Translate, TranslateMap } from "../../utils/translate.ts"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { Activatable, countOptions } from "../activatable/activatableEntry.ts"
import { CombinedActiveMagicalTradition } from "../activatable/magicalTradition.ts"
import {
ImprovementCost,
compareImprovementCost,
createImprovementCost,
fromRaw,
} from "../adventurePoints/improvementCost.ts"
import { All, GetById } from "../getTypes.ts"
@@ -29,7 +34,21 @@ import {
createIdentifierObject,
getCreateIdentifierObject,
} from "../identifier.ts"
import { LibraryEntryContent, createLibraryEntryCreator } from "../libraryEntry.ts"
import { getTextForCantripDuration } from "../libraryEntry/activatableSkill/duration.ts"
import { getTextForEffect } from "../libraryEntry/activatableSkill/effect.ts"
import { Entity } from "../libraryEntry/activatableSkill/entity.ts"
import { parensIf } from "../libraryEntry/activatableSkill/parensIf.ts"
import { getTextForCantripRange } from "../libraryEntry/activatableSkill/range.ts"
import { getTextForTargetCategory } from "../libraryEntry/activatableSkill/targetCategory.ts"
import { ResponsiveTextSize } from "../libraryEntry/responsiveText.ts"
import { SpellsSortOrder } from "../sortOrders.ts"
import {
getTextForFastOneTimePerformanceParameters,
getTextForFastSustainedPerformanceParameters,
getTextForSlowOneTimePerformanceParameters,
getTextForSlowSustainedPerformanceParameters,
} from "./activatableSkill.ts"
import {
cursesImprovementCost,
dominationRitualsImprovementCost,
@@ -45,6 +64,7 @@ import {
isRatedActive,
isRatedWithEnhancementsActive,
} from "./ratedEntry.ts"
import { getTextForCheck } from "./skillCheck.ts"
import { DisplayedActiveSpellwork } from "./spellActive.ts"
import { DisplayedInactiveSpellwork } from "./spellInactive.ts"
@@ -700,3 +720,423 @@ export const filterAndSortDisplayed = <
})(),
)
}
const getTextForProperty = (
deps: {
translate: Translate
translateMap: TranslateMap
getPropertyById: GetById.Static.Property
},
value: PropertyReference,
): LibraryEntryContent => {
const text = (() => {
const staticEntry = deps.getPropertyById(value.id.property)
const staticEntryTranslation = deps.translateMap(staticEntry?.translations)
if (staticEntryTranslation === undefined) {
return ""
}
return staticEntryTranslation.name
})()
return {
label: deps.translate("Property"),
value: text,
}
}
const getTextForTraditions = (
deps: {
translate: Translate
translateMap: TranslateMap
localeCompare: Compare<string>
getMagicalTraditionById: GetById.Static.MagicalTradition
},
value: Traditions,
): LibraryEntryContent => {
const text = (() => {
switch (value.tag) {
case "General":
return deps.translate("General")
case "Specific":
return value.specific
.map(trad =>
deps.translateMap(deps.getMagicalTraditionById(trad.magical_tradition)?.translations),
)
.filter(isNotNullish)
.map(trad => trad.name_for_arcane_spellworks ?? trad.name)
.sort(deps.localeCompare)
.join(", ")
default:
return assertExhaustive(value)
}
})()
return {
label: deps.translate("Traditions"),
value: text,
}
}
const getTraditionNameForArcaneSpellworksById = (
ref: MagicalTraditionReference,
getMagicalTraditionById: GetById.Static.MagicalTradition,
translateMap: TranslateMap,
) => {
const translation = translateMap(getMagicalTraditionById(ref.id.magical_tradition)?.translations)
return translation?.name_for_arcane_spellworks ?? translation?.name
}
/**
* Get a JSON representation of the rules text for a cantrip.
*/
export const getCantripLibraryEntry = createLibraryEntryCreator<
Cantrip,
{
getTargetCategoryById: GetById.Static.TargetCategory
getPropertyById: GetById.Static.Property
getMagicalTraditionById: GetById.Static.MagicalTradition
getCurriculumById: GetById.Static.Curriculum
}
>(
(entry, { getTargetCategoryById, getPropertyById, getMagicalTraditionById, getCurriculumById }) =>
({ translate, translateMap, localeCompare }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
const range = getTextForCantripRange({ translate }, entry.parameters.range, {
responsiveText: ResponsiveTextSize.Full,
})
const duration = getTextForCantripDuration(
{ translate, translateMap },
entry.parameters.duration,
{
responsiveText: ResponsiveTextSize.Full,
},
)
return {
title: translation.name,
className: "cantrip",
content: [
{
label: translate("Effect"),
value: translation.effect,
},
{
label: translate("Range"),
value: range !== translation.range ? `***${range}*** (${translation.range})` : range,
},
{
label: translate("Duration"),
value:
duration !== translation.duration
? `***${duration}*** (${translation.duration})`
: duration,
},
getTextForTargetCategory(
{ translate, translateMap, getTargetCategoryById },
entry.target,
),
getTextForProperty({ translate, translateMap, getPropertyById }, entry.property),
mapNullable(entry.note, note => ({
label: translate("Note"),
value: (() => {
switch (note.tag) {
case "Common":
return note.common.list
.map(academyOrTradition => {
switch (academyOrTradition.tag) {
case "Academy":
return translateMap(
getCurriculumById(academyOrTradition.academy.id.curriculum)
?.translations,
)?.name
case "Tradition": {
return mapNullable(
getTraditionNameForArcaneSpellworksById(
academyOrTradition.tradition,
getMagicalTraditionById,
translateMap,
),
name =>
name +
parensIf(
translateMap(academyOrTradition.tradition.translations)?.note,
),
)
}
default:
return assertExhaustive(academyOrTradition)
}
})
.filter(isNotNullish)
.sort(localeCompare)
.join(", ")
case "Exclusive":
return note.exclusive.traditions
.map(tradition =>
getTraditionNameForArcaneSpellworksById(
tradition,
getMagicalTraditionById,
translateMap,
),
)
.filter(isNotNullish)
.sort(localeCompare)
.join(", ")
default:
return assertExhaustive(note)
}
})(),
})),
],
src: entry.src,
}
},
)
/**
* Get a JSON representation of the rules text for a skill.
*/
export const getSpellLibraryEntry = createLibraryEntryCreator<
Spell,
{
getAttributeById: GetById.Static.Attribute
getDerivedCharacteristicById: GetById.Static.DerivedCharacteristic
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
getTargetCategoryById: GetById.Static.TargetCategory
getPropertyById: GetById.Static.Property
getMagicalTraditionById: GetById.Static.MagicalTradition
}
>(
(
entry,
{
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getPropertyById,
getMagicalTraditionById,
},
) =>
({ translate, translateMap, localeCompare }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
const { castingTime, cost, range, duration } = (() => {
switch (entry.parameters.tag) {
case "OneTime":
return getTextForFastOneTimePerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.one_time,
{
entity: Entity.Spell,
responsiveText: ResponsiveTextSize.Full,
},
)
case "Sustained":
return getTextForFastSustainedPerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.sustained,
{
entity: Entity.Spell,
responsiveText: ResponsiveTextSize.Full,
},
)
default:
return assertExhaustive(entry.parameters)
}
})()
return {
title: translation.name,
className: "spell",
content: [
getTextForCheck({ translate, translateMap, getAttributeById }, entry.check, {
value: entry.check_penalty,
responsiveText: ResponsiveTextSize.Full,
getDerivedCharacteristicById,
}),
...getTextForEffect(translation.effect, translate),
{
label: translate("Casting Time"),
value:
castingTime !== translation.casting_time.full
? `***${castingTime}*** (${translation.casting_time.full})`
: castingTime,
},
{
label: translate("AE Cost"),
value:
cost !== translation.cost.full ? `***${cost}*** (${translation.cost.full})` : cost,
},
{
label: translate("Range"),
value:
range !== translation.range.full
? `***${range}*** (${translation.range.full})`
: range,
},
{
label: translate("Duration"),
value:
duration !== translation.duration.full
? `***${duration}*** (${translation.duration.full})`
: duration,
},
getTextForTargetCategory(
{ translate, translateMap, getTargetCategoryById },
entry.target,
),
getTextForProperty({ translate, translateMap, getPropertyById }, entry.property),
getTextForTraditions(
{ translate, translateMap, localeCompare, getMagicalTraditionById },
entry.traditions,
),
createImprovementCost(translate, entry.improvement_cost),
],
src: entry.src,
}
},
)
/**
* Get a JSON representation of the rules text for a ritual.
*/
export const getRitualLibraryEntry = createLibraryEntryCreator<
Ritual,
{
getAttributeById: GetById.Static.Attribute
getDerivedCharacteristicById: GetById.Static.DerivedCharacteristic
getSkillModificationLevelById: GetById.Static.SkillModificationLevel
getTargetCategoryById: GetById.Static.TargetCategory
getPropertyById: GetById.Static.Property
getMagicalTraditionById: GetById.Static.MagicalTradition
}
>(
(
entry,
{
getAttributeById,
getDerivedCharacteristicById,
getSkillModificationLevelById,
getTargetCategoryById,
getPropertyById,
getMagicalTraditionById,
},
) =>
({ translate, translateMap, localeCompare }) => {
const translation = translateMap(entry.translations)
if (translation === undefined) {
return undefined
}
const { castingTime, cost, range, duration } = (() => {
switch (entry.parameters.tag) {
case "OneTime":
return getTextForSlowOneTimePerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.one_time,
{
entity: Entity.Ritual,
responsiveText: ResponsiveTextSize.Full,
},
)
case "Sustained":
return getTextForSlowSustainedPerformanceParameters(
{
getSkillModificationLevelById,
translate,
translateMap,
},
entry.parameters.sustained,
{
entity: Entity.Ritual,
responsiveText: ResponsiveTextSize.Full,
},
)
default:
return assertExhaustive(entry.parameters)
}
})()
return {
title: translation.name,
className: "ritual",
content: [
getTextForCheck({ translate, translateMap, getAttributeById }, entry.check, {
value: entry.check_penalty,
responsiveText: ResponsiveTextSize.Full,
getDerivedCharacteristicById,
}),
...getTextForEffect(translation.effect, translate),
{
label: translate("Ritual Time"),
value:
castingTime !== translation.casting_time.full
? `***${castingTime}*** (${translation.casting_time.full})`
: castingTime,
},
{
label: translate("AE Cost"),
value:
cost !== translation.cost.full ? `***${cost}*** (${translation.cost.full})` : cost,
},
{
label: translate("Range"),
value:
range !== translation.range.full
? `***${range}*** (${translation.range.full})`
: range,
},
{
label: translate("Duration"),
value:
duration !== translation.duration.full
? `***${duration}*** (${translation.duration.full})`
: duration,
},
getTextForTargetCategory(
{ translate, translateMap, getTargetCategoryById },
entry.target,
),
getTextForProperty({ translate, translateMap, getPropertyById }, entry.property),
getTextForTraditions(
{ translate, translateMap, localeCompare, getMagicalTraditionById },
entry.traditions,
),
createImprovementCost(translate, entry.improvement_cost),
],
src: entry.src,
}
},
)
+13
View File
@@ -7,6 +7,7 @@ import {
countByMany,
ensureNonEmpty,
filterNonNullable,
groupBy,
partition,
range,
rangeSafe,
@@ -245,3 +246,15 @@ describe("someCount", () => {
assert.equal(result, false)
})
})
describe("groupBy", () => {
it("should return an empty array for an empty input", () => {
const result = groupBy([], (a, b) => a === b)
assert.deepEqual(result, [])
})
it("should group adjacent elements that are equal", () => {
const result = groupBy([1, 1, 2, 2, 2, 3, 4, 4, 4, 4], (a, b) => a === b)
assert.deepEqual(result, [[1, 1], [2, 2, 2], [3], [4, 4, 4, 4]])
})
})
+17
View File
@@ -1,3 +1,4 @@
import { Equality } from "./compare.ts"
import { isNotNullish } from "./nullable.ts"
/**
@@ -183,3 +184,19 @@ export const removeIndex = <T>(arr: T[], index: number): T[] => [
...arr.slice(0, index),
...arr.slice(index + 1),
]
/**
* Returns a new array, where adjacent elements that are considered equal by the
* given equality function are grouped together. Calling the `flat` method on
* the result will return the original array.
*/
export const groupBy = <T>(arr: T[], equal: Equality<T>): T[][] =>
arr.reduce<T[][]>((acc, value) => {
const lastGroup = acc[acc.length - 1]
if (lastGroup?.[0] !== undefined && equal(lastGroup[0], value)) {
lastGroup.push(value)
} else {
acc.push([value])
}
return acc
}, [])
+27 -6
View File
@@ -18,12 +18,7 @@ const matchSystemLocaleToSupported = (available: string[], system: string) => {
/**
* Translates a given key into a string, optionally with parameters.
*/
export type Translate = <K extends keyof UI>(
key: K,
...options: UI[K] extends PluralizationCategories
? [count: number, ...params: (string | number)[]]
: [...params: (string | number)[]]
) => string
export type Translate = <K extends keyof UI>(key: K, ...params: (string | number)[]) => string
const isPluralizationCategories = (
value: string | PluralizationCategories | VaryBySystem,
@@ -40,6 +35,24 @@ const pluralType: {
"You are missing {0} Adventure Points to do this.": "cardinal",
"since the {0}. printing": "ordinal",
"removed in {0}. printing": "ordinal",
"{0} actions": "cardinal",
"{0} hours": "cardinal",
"{0} minutes": "cardinal",
"{0} rounds": "cardinal",
"{0} seduction actions": "cardinal",
", {0} of which are permanent": "cardinal",
"{0} centuries": "cardinal",
"{0} combat rounds": "cardinal",
"{0} days": "cardinal",
"{0} months": "cardinal",
"{0} mos.": "cardinal",
"{0} seconds": "cardinal",
"{0} weeks": "cardinal",
"{0} wks.": "cardinal",
"{0} years": "cardinal",
"{0} yrs.": "cardinal",
"{0} miles": "cardinal",
"{0} yards": "cardinal",
}
/**
@@ -96,6 +109,14 @@ export const createTranslate = (
return translate
}
/**
* A mocked translate function.
*/
export const translateMock: Translate = <K extends keyof UI>(
key: K,
...options: (string | number)[]
) => insertParams(key, options)
/**
* Selects a value from a locale dictionary based on the selected locale.
*/