From 739264f725d76691d60e240a2f9cc94494b9ae61 Mon Sep 17 00:00:00 2001 From: Lukas Obermann Date: Thu, 29 Feb 2024 18:56:45 +0100 Subject: [PATCH] fix: check available adventure points when adding rated entries --- .../Activatable/activatableSkillUtils.ts | 36 - src/main_window/hooks/ratedActions.ts | 286 +++++++ .../entities/InlineLibrarySkill.tsx | 48 +- .../attributes/AttributesListItem.tsx | 60 +- .../CloseCombatTechniquesListItem.tsx | 82 +- .../combatTechniques/CombatTechniques.tsx | 121 +-- .../RangedCombatTechniquesListItem.tsx | 77 +- .../ActiveBlessingsListItem.tsx | 7 +- .../ActiveCeremoniesListItem.tsx | 45 +- .../ActiveLiturgicalChantsListItem.tsx | 45 +- .../InactiveBlessingsListItem.tsx | 6 +- .../InactiveCeremoniesListItem.tsx | 10 +- .../InactiveLiturgicalChantsListItem.tsx | 14 +- .../liturgicalChants/LiturgicalChants.tsx | 118 +-- .../character/skills/SkillButtons.tsx | 71 +- .../character/skills/SkillGroup.tsx | 21 - .../character/skills/SkillListItem.tsx | 133 ++- .../character/skills/SkillRating.tsx | 5 +- .../characters/character/skills/Skills.tsx | 60 +- .../spells/ActiveAnimistPowersListItem.tsx | 55 +- .../spells/ActiveCantripsListItem.tsx | 7 +- .../character/spells/ActiveCursesListItem.tsx | 38 +- .../ActiveDominationRitualsListItem.tsx | 38 +- .../ActiveElvenMagicalSongsListItem.tsx | 38 +- .../spells/ActiveGeodeRitualsListItem.tsx | 38 +- .../spells/ActiveJesterTricksListItem.tsx | 38 +- .../spells/ActiveMagicalActionsListItem.tsx | 24 +- .../spells/ActiveMagicalDancesListItem.tsx | 38 +- .../spells/ActiveMagicalMelodiesListItem.tsx | 38 +- .../spells/ActiveRitualsListItem.tsx | 45 +- .../character/spells/ActiveSpellsListItem.tsx | 47 +- .../spells/ActiveZibiljaRitualsListItem.tsx | 38 +- .../spells/InactiveAnimistPowersListItem.tsx | 30 +- .../spells/InactiveCantripsListItem.tsx | 6 +- .../spells/InactiveCursesListItem.tsx | 13 +- .../InactiveDominationRitualsListItem.tsx | 13 +- .../InactiveElvenMagicalSongsListItem.tsx | 13 +- .../spells/InactiveGeodeRitualsListItem.tsx | 13 +- .../spells/InactiveJesterTricksListItem.tsx | 13 +- .../spells/InactiveMagicalActionsListItem.tsx | 10 +- .../spells/InactiveMagicalDancesListItem.tsx | 13 +- .../InactiveMagicalMelodiesListItem.tsx | 13 +- .../spells/InactiveRitualsListItem.tsx | 11 +- .../spells/InactiveSpellsListItem.tsx | 11 +- .../spells/InactiveZibiljaRitualsListItem.tsx | 13 +- .../characters/character/spells/Spells.tsx | 307 +------ .../selectors/adventurePointSelectors.ts | 14 +- .../selectors/attributeSelectors.ts | 12 +- .../selectors/combatTechniquesSelectors.ts | 7 +- .../derivedCharacteristicsSelectors.ts | 20 +- .../selectors/liturgicalChantSelectors.ts | 25 +- src/main_window/selectors/profileSelectors.ts | 4 +- src/main_window/selectors/skillsSelectors.ts | 5 +- src/main_window/selectors/spellSelectors.ts | 60 +- .../slices/activatableRatedSlice.ts | 146 ++-- .../activatableRatedWithEnhancementsSlice.ts | 135 +-- src/main_window/slices/attributesSlice.ts | 31 +- src/main_window/slices/ceremoniesSlice.ts | 6 +- src/main_window/slices/characterSlice.ts | 115 ++- .../slices/closeCombatTechniqueSlice.ts | 25 +- src/main_window/slices/databaseSlice.ts | 82 ++ .../slices/derivedCharacteristicsSlice.ts | 6 +- .../slices/liturgicalChantsSlice.ts | 6 +- .../magicalActions/animistPowersSlice.ts | 35 +- .../slices/magicalActions/cursesSlice.ts | 10 +- .../magicalActions/dominationRitualsSlice.ts | 10 +- .../magicalActions/elvenMagicalSongsSlice.ts | 6 +- .../magicalActions/geodeRitualsSlice.ts | 10 +- .../magicalActions/jesterTricksSlice.ts | 6 +- .../magicalActions/magicalDancesSlice.ts | 6 +- .../magicalActions/magicalMelodiesSlice.ts | 6 +- .../magicalActions/magicalRunesSlice.ts | 30 + .../magicalActions/zibiljaRitualsSlice.ts | 6 +- src/main_window/slices/personalDataSlice.ts | 6 +- src/main_window/slices/professionSlice.ts | 6 +- src/main_window/slices/raceSlice.ts | 6 +- .../slices/rangedCombatTechniqueSlice.ts | 25 +- src/main_window/slices/ratedSlice.ts | 142 +--- src/main_window/slices/ritualsSlice.ts | 6 +- src/main_window/slices/rulesSlice.ts | 6 +- src/main_window/slices/skillsSlice.ts | 21 +- src/main_window/slices/spellsSlice.ts | 6 +- .../slices/tinyActivatableSlice.ts | 18 +- .../RecommendedReference.tsx | 8 +- .../domain/adventurePoints/ratedEntry.test.ts | 7 +- .../domain/adventurePoints/ratedEntry.ts | 67 +- src/shared/domain/character.test.ts | 11 +- src/shared/domain/character.ts | 38 +- src/shared/domain/culture.ts | 27 + src/shared/domain/equipment.ts | 4 + src/shared/domain/identifier.ts | 768 +++++++++++++++--- src/shared/domain/pact.ts | 30 + src/shared/domain/race.ts | 32 + src/shared/domain/rated/animistPower.ts | 27 + src/shared/domain/rated/attribute.ts | 6 + src/shared/domain/rated/attributeBounds.ts | 16 +- src/shared/domain/rated/combatTechnique.ts | 19 +- .../domain/rated/combatTechniqueBounds.ts | 5 +- src/shared/domain/rated/enhancement.ts | 17 +- src/shared/domain/rated/liturgicalChant.ts | 25 +- .../domain/rated/liturgicalChantActive.ts | 3 +- .../rated/liturgicalChantActiveBounds.ts | 14 +- src/shared/domain/rated/magicalActions.ts | 15 + src/shared/domain/rated/magicalRune.ts | 21 + src/shared/domain/rated/primaryAttribute.ts | 40 + src/shared/domain/rated/ratedDependency.ts | 63 +- src/shared/domain/rated/ratedEntry.ts | 32 +- src/shared/domain/rated/skill.ts | 4 +- src/shared/domain/rated/skillBounds.ts | 9 +- src/shared/domain/rated/spell.ts | 40 +- src/shared/domain/rated/spellActive.ts | 42 +- src/shared/domain/rated/spellActiveBounds.ts | 15 +- src/shared/domain/rated/spellInactive.ts | 26 +- src/shared/domain/sex.ts | 20 + src/shared/domain/socialStatus.ts | 5 + .../domain/sources/publicationDependency.ts | 27 + src/shared/domain/state.ts | 24 + 117 files changed, 3002 insertions(+), 1777 deletions(-) delete mode 100644 src/App/Utilities/Activatable/activatableSkillUtils.ts create mode 100644 src/main_window/hooks/ratedActions.ts delete mode 100644 src/main_window/routes/characters/character/skills/SkillGroup.tsx create mode 100644 src/main_window/slices/magicalActions/magicalRunesSlice.ts create mode 100644 src/shared/domain/equipment.ts create mode 100644 src/shared/domain/rated/animistPower.ts create mode 100644 src/shared/domain/rated/magicalRune.ts create mode 100644 src/shared/domain/sources/publicationDependency.ts diff --git a/src/App/Utilities/Activatable/activatableSkillUtils.ts b/src/App/Utilities/Activatable/activatableSkillUtils.ts deleted file mode 100644 index c0b0278b..00000000 --- a/src/App/Utilities/Activatable/activatableSkillUtils.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Filter and count `ActivatableSkill` entries. - * - * @author Lukas Obermann - */ - -import { filter, OrderedMap, size } from "../../../Data/OrderedMap" -import { Record } from "../../../Data/Record" -import { ActivatableSkillDependent } from "../../Models/ActiveEntries/ActivatableSkillDependent" -import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel" -import { pipe } from "../pipe" - -type ActivatableSkillEntriesAccessor = - (hero: HeroModelRecord) => OrderedMap> - -const { active } = ActivatableSkillDependent.AL - -/** - * Get all active `ActivatableSkillDependent` entries from the specified domain. - */ -export const getActiveSkillEntries = - (domain: "spells" | "liturgicalChants") => - pipe ( - HeroModel.AL[domain] as ActivatableSkillEntriesAccessor, - filter (active) - ) - -/** - * Count all active skills from the specified domain. - */ -export const countActiveSkillEntries = - (domain: "spells" | "liturgicalChants") => - pipe ( - getActiveSkillEntries (domain), - size - ) diff --git a/src/main_window/hooks/ratedActions.ts b/src/main_window/hooks/ratedActions.ts new file mode 100644 index 00000000..4c2d71ae --- /dev/null +++ b/src/main_window/hooks/ratedActions.ts @@ -0,0 +1,286 @@ +import { ActionCreatorWithPayload } from "@reduxjs/toolkit" +import { useCallback } from "react" +import { + ImprovementCost, + adventurePointsForActivation, + adventurePointsForIncrement, + adventurePointsForRange, +} from "../../shared/domain/adventurePoints/improvementCost.ts" +import { useTranslate } from "../../shared/hooks/translate.ts" +import { isOk } from "../../shared/utils/result.ts" +import { Translate } from "../../shared/utils/translate.ts" +import { showAlert } from "../slices/alertsSlice.ts" +import { + getAlertForReason, + useAreEnoughAdventurePointsAvailableToBuy, +} from "./areEnoughAdventurePointsAvailableToBuy.ts" +import { useAppDispatch } from "./redux.ts" + +const useHandleAdd = ( + id: number, + improvementCost: ImprovementCost, + createAddAction: ActionCreatorWithPayload<{ id: number }>, + dispatch: ReturnType, + areEnoughAdventurePointsAvailableToBuy: ReturnType< + typeof useAreEnoughAdventurePointsAvailableToBuy + >, + translate: Translate, +) => + useCallback((): void => { + const enoughAP = areEnoughAdventurePointsAvailableToBuy( + adventurePointsForActivation(improvementCost), + ) + + if (enoughAP !== undefined) { + if (isOk(enoughAP)) { + dispatch(createAddAction({ id })) + } else { + dispatch(showAlert(getAlertForReason(enoughAP.error, translate))) + } + } + }, [ + areEnoughAdventurePointsAvailableToBuy, + createAddAction, + dispatch, + id, + improvementCost, + translate, + ]) + +const useHandleAddPoint = ( + id: number, + value: number, + improvementCost: ImprovementCost, + createIncrementAction: ActionCreatorWithPayload<{ id: number }>, + dispatch: ReturnType, + areEnoughAdventurePointsAvailableToBuy: ReturnType< + typeof useAreEnoughAdventurePointsAvailableToBuy + >, + translate: Translate, +) => + useCallback((): void => { + const enoughAP = areEnoughAdventurePointsAvailableToBuy( + adventurePointsForIncrement(improvementCost, value), + ) + + if (enoughAP !== undefined) { + if (isOk(enoughAP)) { + dispatch(createIncrementAction({ id })) + } else { + dispatch(showAlert(getAlertForReason(enoughAP.error, translate))) + } + } + }, [ + areEnoughAdventurePointsAvailableToBuy, + createIncrementAction, + dispatch, + id, + improvementCost, + translate, + value, + ]) + +const useHandleSetToMaximumPoints = ( + id: number, + value: number, + maximum: number, + improvementCost: ImprovementCost, + createSetAction: ActionCreatorWithPayload<{ id: number; value: number }>, + dispatch: ReturnType, + areEnoughAdventurePointsAvailableToBuy: ReturnType< + typeof useAreEnoughAdventurePointsAvailableToBuy + >, + translate: Translate, +) => + useCallback((): void => { + const enoughAP = areEnoughAdventurePointsAvailableToBuy( + adventurePointsForRange(improvementCost, value, maximum), + ) + + if (enoughAP !== undefined) { + if (isOk(enoughAP)) { + dispatch(createSetAction({ id, value: maximum })) + } else { + dispatch(showAlert(getAlertForReason(enoughAP.error, translate))) + } + } + }, [ + areEnoughAdventurePointsAvailableToBuy, + createSetAction, + dispatch, + id, + improvementCost, + maximum, + translate, + value, + ]) + +const useHandleSetToMinimumPoints = ( + id: number, + minimum: number, + createSetAction: ActionCreatorWithPayload<{ id: number; value: number }>, + dispatch: ReturnType, +) => + useCallback((): void => { + dispatch(createSetAction({ id, value: minimum })) + }, [createSetAction, dispatch, id, minimum]) + +const useHandleRemovePoint = ( + id: number, + createDecrementAction: ActionCreatorWithPayload<{ id: number }>, + dispatch: ReturnType, +) => + useCallback(() => { + dispatch(createDecrementAction({ id })) + }, [createDecrementAction, dispatch, id]) + +const useHandleRemove = ( + id: number, + createRemoveAction: ActionCreatorWithPayload<{ id: number }>, + dispatch: ReturnType, +) => + useCallback(() => { + dispatch(createRemoveAction({ id })) + }, [createRemoveAction, dispatch, id]) + +/** + * Returns a set of callbacks for interaction with rated entries. + */ +export const useRatedActions = ( + id: number, + value: number, + maximum: number, + minimum: number, + improvementCost: ImprovementCost, + createIncrementAction: ActionCreatorWithPayload<{ id: number }>, + createDecrementAction: ActionCreatorWithPayload<{ id: number }>, + createSetAction: ActionCreatorWithPayload<{ id: number; value: number }>, +) => { + const dispatch = useAppDispatch() + const areEnoughAdventurePointsAvailableToBuy = useAreEnoughAdventurePointsAvailableToBuy() + const translate = useTranslate() + + const handleAddPoint = useHandleAddPoint( + id, + value, + improvementCost, + createIncrementAction, + dispatch, + areEnoughAdventurePointsAvailableToBuy, + translate, + ) + + const handleSetToMaximumPoints = useHandleSetToMaximumPoints( + id, + value, + maximum, + improvementCost, + createSetAction, + dispatch, + areEnoughAdventurePointsAvailableToBuy, + translate, + ) + + const handleSetToMinimumPoints = useHandleSetToMinimumPoints( + id, + minimum, + createSetAction, + dispatch, + ) + + const handleRemovePoint = useHandleRemovePoint(id, createDecrementAction, dispatch) + + return { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + } +} + +/** + * Returns a set of callbacks for interaction with active activatable rated + * entries. + */ +export const useActiveActivatableActions = ( + id: number, + value: number, + maximum: number, + minimum: number, + improvementCost: ImprovementCost, + createIncrementAction: ActionCreatorWithPayload<{ id: number }>, + createDecrementAction: ActionCreatorWithPayload<{ id: number }>, + createSetAction: ActionCreatorWithPayload<{ id: number; value: number }>, + createRemoveAction: ActionCreatorWithPayload<{ id: number }>, +) => { + const dispatch = useAppDispatch() + const areEnoughAdventurePointsAvailableToBuy = useAreEnoughAdventurePointsAvailableToBuy() + const translate = useTranslate() + + const handleAddPoint = useHandleAddPoint( + id, + value, + improvementCost, + createIncrementAction, + dispatch, + areEnoughAdventurePointsAvailableToBuy, + translate, + ) + + const handleSetToMaximumPoints = useHandleSetToMaximumPoints( + id, + value, + maximum, + improvementCost, + createSetAction, + dispatch, + areEnoughAdventurePointsAvailableToBuy, + translate, + ) + + const handleSetToMinimumPoints = useHandleSetToMinimumPoints( + id, + minimum, + createSetAction, + dispatch, + ) + + const handleRemovePoint = useHandleRemovePoint(id, createDecrementAction, dispatch) + + const handleRemove = useHandleRemove(id, createRemoveAction, dispatch) + + return { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } +} + +/** + * Returns a set of callbacks for interaction with inactive activatable rated + * entries. + */ +export const useInactiveActivatableActions = ( + id: number, + improvementCost: ImprovementCost, + createAddAction: ActionCreatorWithPayload<{ id: number }>, +) => { + const dispatch = useAppDispatch() + const areEnoughAdventurePointsAvailableToBuy = useAreEnoughAdventurePointsAvailableToBuy() + const translate = useTranslate() + + const handleAdd = useHandleAdd( + id, + improvementCost, + createAddAction, + dispatch, + areEnoughAdventurePointsAvailableToBuy, + translate, + ) + + return { + handleAdd, + } +} diff --git a/src/main_window/inlineLibrary/entities/InlineLibrarySkill.tsx b/src/main_window/inlineLibrary/entities/InlineLibrarySkill.tsx index 29fec6f4..a9942bef 100644 --- a/src/main_window/inlineLibrary/entities/InlineLibrarySkill.tsx +++ b/src/main_window/inlineLibrary/entities/InlineLibrarySkill.tsx @@ -1,4 +1,4 @@ -import { FC } from "react" +import { FC, useMemo } from "react" import { useLocaleCompare } from "../../../shared/hooks/localeCompare.ts" import { useTranslate } from "../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../shared/hooks/translateMap.ts" @@ -6,6 +6,7 @@ import { isNotNullish } from "../../../shared/utils/nullable.ts" import { assertExhaustive } from "../../../shared/utils/typeSafety.ts" import { useAppSelector } from "../../hooks/redux.ts" import { + selectNewApplicationsAndUsesCache, selectStaticAttributes, selectStaticBlessedTraditions, selectStaticDiseases, @@ -37,17 +38,30 @@ export const InlineLibrarySkill: FC = ({ id }) => { const regions = useAppSelector(selectStaticRegions) const entry = useAppSelector(selectStaticSkills)[id] const translation = translateMap(entry?.translations) + const cache = useAppSelector(selectNewApplicationsAndUsesCache) + + const newApplications = useMemo( + () => + (entry === undefined ? [] : cache.newApplications[entry.id] ?? []) + .map(x => translateMap(x.data.translations)?.name) + .filter(isNotNullish) + .sort(localeCompare), + [cache.newApplications, entry, localeCompare, translateMap], + ) + + const uses = useMemo( + () => + (entry === undefined ? [] : cache.uses[entry.id] ?? []) + .map(x => translateMap(x.data.translations)?.name) + .filter(isNotNullish) + .sort(localeCompare), + [cache.uses, entry, localeCompare, translateMap], + ) if (entry === undefined || translation === undefined) { return } - // TODO: Implement - const newApplications = [] - - // TODO: Implement - const uses = [] - const applications = (() => { switch (entry.applications.tag) { case "Derived": @@ -86,14 +100,18 @@ export const InlineLibrarySkill: FC = ({ id }) => { = props => { const translateMap = useTranslateMap() const translations = translateMap(attribute.static.translations) - const dispatch = useAppDispatch() - const { dynamic: { value }, static: { id }, - maximum: max, + maximum, isDecreasable, isIncreasable, } = attribute - const valueHeader = isInCharacterCreation ? `${value} / ${max}` : value + const valueHeader = isInCharacterCreation ? `${value} / ${maximum}` : value - const handleAdd = useCallback(() => dispatch(incrementAttribute(id)), [dispatch, id]) + const { handleAddPoint, handleRemovePoint, handleSetToMaximumPoints, handleSetToMinimumPoints } = + useRatedActions( + id, + value, + maximum ?? value, + minimumAttributeValue, + attributeImprovementCost, + incrementAttribute, + decrementAttribute, + setAttribute, + ) - const handleRemove = useCallback(() => dispatch(decrementAttribute(id)), [dispatch, id]) + const handleAdd = useCallback( + (event: MouseEvent) => { + if (isIncreasable) { + if (event.shiftKey && maximum !== undefined) { + handleSetToMaximumPoints() + } else { + handleAddPoint() + } + } + }, + [handleAddPoint, handleSetToMaximumPoints, isIncreasable, maximum], + ) + + const handleRemove = useCallback( + (event: MouseEvent) => { + if (isDecreasable) { + if (event.shiftKey) { + handleSetToMinimumPoints() + } else { + handleRemovePoint() + } + } + }, + [handleRemovePoint, handleSetToMinimumPoints, isDecreasable], + ) return ( = props => { } tooltipMargin={11} > - {isInCharacterCreation ? : null} + {isInCharacterCreation ? : null} void - removePoint: (id: number) => void + closeCombatTechnique: DisplayedCloseCombatTechnique } const CloseCombatTechniquesListItem: FC = props => { const { insertTopMargin, - id, - name, - sr, - primary, - ic, - addDisabled, - removeDisabled, - at, - pa, - addPoint, - removePoint, + closeCombatTechnique: { + static: { id, primary_attribute, improvement_cost, translations }, + dynamic: { value }, + maximum, + minimum, + isIncreasable, + isDecreasable, + attackBase, + parryBase, + }, } = props const translate = useTranslate() @@ -57,6 +52,20 @@ const CloseCombatTechniquesListItem: FC = props => { const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId) const canRemove = useAppSelector(selectCanRemove) + const { name = "???" } = translateMap(translations) ?? {} + + const { handleAddPoint, handleRemovePoint, handleSetToMaximumPoints, handleSetToMinimumPoints } = + useRatedActions( + id, + value, + maximum, + minimum, + fromRaw(improvement_cost), + incrementCloseCombatTechnique, + decrementCloseCombatTechnique, + setCloseCombatTechnique, + ) + const handleSelectForInfo = useCallback( () => dispatch( @@ -67,11 +76,11 @@ const CloseCombatTechniquesListItem: FC = props => { const attributes = useAppSelector(selectStaticAttributes) - const primaryStr = primary + const primaryStr = primary_attribute .map(ref => translateMap(attributes[ref.id.attribute]?.translations)?.abbreviation ?? "") .join("/") - const customClassName = `attr--${primary.map(ref => ref.id.attribute).join("-")}` + const customClassName = `attr--${primary_attribute.map(ref => ref.id.attribute).join("-")}` const primaryClassName = `primary ${customClassName}` @@ -87,28 +96,27 @@ const CloseCombatTechniquesListItem: FC = props => { - - + + diff --git a/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx b/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx index 83a92cd5..96a2014c 100644 --- a/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx +++ b/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx @@ -26,14 +26,6 @@ import { DisplayedCombatTechnique, selectVisibleCombatTechniques, } from "../../../../selectors/combatTechniquesSelectors.ts" -import { - decrementCloseCombatTechnique, - incrementCloseCombatTechnique, -} from "../../../../slices/closeCombatTechniqueSlice.ts" -import { - decrementRangedCombatTechnique, - incrementRangedCombatTechnique, -} from "../../../../slices/rangedCombatTechniqueSlice.ts" import { changeCombatTechniquesSortOrder, selectCombatTechniquesSortOrder, @@ -100,26 +92,6 @@ export const CombatTechniques: FC = () => { [filterText, localeCompare, sortOrder, translateMap, visibleCombatTechniques], ) - const handleAddCloseCombatTechniquePoint = useCallback( - (id: number) => dispatch(incrementCloseCombatTechnique(id)), - [dispatch], - ) - - const handleRemoveCloseCombatTechniquePoint = useCallback( - (id: number) => dispatch(decrementCloseCombatTechnique(id)), - [dispatch], - ) - - const handleAddRangedCombatTechniquePoint = useCallback( - (id: number) => dispatch(incrementRangedCombatTechnique(id)), - [dispatch], - ) - - const handleRemoveRangedCombatTechniquePoint = useCallback( - (id: number) => dispatch(decrementRangedCombatTechnique(id)), - [dispatch], - ) - return ( @@ -146,35 +118,22 @@ export const CombatTechniques: FC = () => {
- - {translate("combattechniques.header.name")} + {translate("Name")} + {translate("Group")} + + {translate("CTR")} - - {translate("combattechniques.header.group")} + + {translate("IC")} - - {translate("combattechniques.header.combattechniquerating")} + + {translate("P")} - - {translate("combattechniques.header.improvementcost")} + + {translate("AT")} - - {translate("combattechniques.header.primaryattribute")} - - - {translate("combattechniques.header.attack")} - - - {translate("combattechniques.header.parry")} + + {translate("PA")} {canRemove ? : null} @@ -184,44 +143,26 @@ export const CombatTechniques: FC = () => { {list.length > 0 ? ( {list.map((x, i) => { - const translation = translateMap(x.static.translations) - - if (x.kind === "close") { - return ( - - ) + switch (x.kind) { + case "close": + return ( + + ) + case "ranged": + return ( + + ) + default: + return assertExhaustive(x) } - - return ( - - ) })} ) : ( diff --git a/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx b/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx index 24a79937..26023ae9 100644 --- a/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx +++ b/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx @@ -1,20 +1,26 @@ -import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences" import { FC, memo, useCallback } from "react" import { ListItem } from "../../../../../shared/components/list/ListItem.tsx" import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx" import { ListItemName } from "../../../../../shared/components/list/ListItemName.tsx" import { ListItemSeparator } from "../../../../../shared/components/list/ListItemSeparator.tsx" import { ListItemValues } from "../../../../../shared/components/list/ListItemValues.tsx" -import { ImprovementCost } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useRatedActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" +import { DisplayedRangedCombatTechnique } from "../../../../selectors/combatTechniquesSelectors.ts" import { selectStaticAttributes } from "../../../../slices/databaseSlice.ts" import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { + decrementRangedCombatTechnique, + incrementRangedCombatTechnique, + setRangedCombatTechnique, +} from "../../../../slices/rangedCombatTechniqueSlice.ts" import { SkillAdditionalValues } from "../skills/SkillAdditionalValues.tsx" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillImprovementCost } from "../skills/SkillImprovementCost.tsx" @@ -22,31 +28,21 @@ import { SkillRating } from "../skills/SkillRating.tsx" type Props = { insertTopMargin?: boolean - id: number - name: string - sr: number - primary: AttributeReference[] - ic: ImprovementCost - addDisabled: boolean - removeDisabled: boolean - at: number - addPoint: (id: number) => void - removePoint: (id: number) => void + rangedCombatTechnique: DisplayedRangedCombatTechnique } const RangedCombatTechniquesListItem: FC = props => { const { insertTopMargin, - id, - name, - sr, - primary, - ic, - addDisabled, - removeDisabled, - at, - addPoint, - removePoint, + rangedCombatTechnique: { + static: { id, primary_attribute, improvement_cost, translations }, + dynamic: { value }, + maximum, + minimum, + isIncreasable, + isDecreasable, + attackBase, + }, } = props const translate = useTranslate() @@ -55,6 +51,20 @@ const RangedCombatTechniquesListItem: FC = props => { const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId) const canRemove = useAppSelector(selectCanRemove) + const { name = "???" } = translateMap(translations) ?? {} + + const { handleAddPoint, handleRemovePoint, handleSetToMaximumPoints, handleSetToMinimumPoints } = + useRatedActions( + id, + value, + maximum, + minimum, + fromRaw(improvement_cost), + incrementRangedCombatTechnique, + decrementRangedCombatTechnique, + setRangedCombatTechnique, + ) + const handleSelectForInfo = useCallback( () => dispatch( @@ -65,11 +75,11 @@ const RangedCombatTechniquesListItem: FC = props => { const attributes = useAppSelector(selectStaticAttributes) - const primaryStr = primary + const primaryStr = primary_attribute .map(ref => translateMap(attributes[ref.id.attribute]?.translations)?.abbreviation ?? "") .join("/") - const customClassName = `attr--${primary.map(ref => ref.id.attribute).join("-")}` + const customClassName = `attr--${primary_attribute.map(ref => ref.id.attribute).join("-")}` const primaryClassName = `primary ${customClassName}` @@ -85,12 +95,12 @@ const RangedCombatTechniquesListItem: FC = props => { - - + + = props => { /> diff --git a/src/main_window/routes/characters/character/liturgicalChants/ActiveBlessingsListItem.tsx b/src/main_window/routes/characters/character/liturgicalChants/ActiveBlessingsListItem.tsx index 9f0e06ca..02ea5d39 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/ActiveBlessingsListItem.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/ActiveBlessingsListItem.tsx @@ -45,6 +45,10 @@ const ActiveBlessingsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const handleRemove = useCallback(() => { + remove(id) + }, [remove, id]) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Blessing", blessing: id })), [dispatch, id], @@ -71,8 +75,7 @@ const ActiveBlessingsListItem: FC = props => { diff --git a/src/main_window/routes/characters/character/liturgicalChants/ActiveCeremoniesListItem.tsx b/src/main_window/routes/characters/character/liturgicalChants/ActiveCeremoniesListItem.tsx index 3b18a7ec..51b3aed3 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/ActiveCeremoniesListItem.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/ActiveCeremoniesListItem.tsx @@ -11,9 +11,16 @@ import { LiturgiesSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" import { selectActiveBlessedTradition } from "../../../../selectors/traditionSelectors.ts" +import { + decrementCeremony, + incrementCeremony, + removeCeremony, + setCeremony, +} from "../../../../slices/ceremoniesSlice.ts" import { selectStaticAspects } from "../../../../slices/databaseSlice.ts" import { changeInlineLibraryEntry, @@ -29,9 +36,6 @@ type Props = { insertTopMargin?: boolean ceremony: DisplayedActiveCeremony sortOrder: LiturgiesSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveCeremoniesListItem: FC = props => { @@ -40,13 +44,12 @@ const ActiveCeremoniesListItem: FC = props => { ceremony: { dynamic: { value }, static: { id, check, check_penalty, traditions, improvement_cost, translations }, + maximum, + minimum, isDecreasable, isIncreasable, }, sortOrder, - addPoint, - removePoint, - remove, } = props const translate = useTranslate() @@ -60,6 +63,24 @@ const ActiveCeremoniesListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + id, + value, + maximum, + minimum ?? 0, + fromRaw(improvement_cost), + incrementCeremony, + decrementCeremony, + setCeremony, + removeCeremony, + ) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Ceremony", ceremony: id })), [dispatch, id], @@ -95,19 +116,19 @@ const ActiveCeremoniesListItem: FC = props => { } /> - + 0 ? handleSetToMinimumPoints : undefined} + decrementIsRemove={value === 0} selectForInfo={handleSelectForInfo} /> diff --git a/src/main_window/routes/characters/character/liturgicalChants/ActiveLiturgicalChantsListItem.tsx b/src/main_window/routes/characters/character/liturgicalChants/ActiveLiturgicalChantsListItem.tsx index fbb2f755..25d8bd33 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/ActiveLiturgicalChantsListItem.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/ActiveLiturgicalChantsListItem.tsx @@ -11,6 +11,7 @@ import { LiturgiesSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" import { selectActiveBlessedTradition } from "../../../../selectors/traditionSelectors.ts" @@ -19,6 +20,12 @@ import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { + decrementLiturgicalChant, + incrementLiturgicalChant, + removeLiturgicalChant, + setLiturgicalChant, +} from "../../../../slices/liturgicalChantsSlice.ts" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillCheck } from "../skills/SkillCheck.tsx" import { SkillFill } from "../skills/SkillFill.tsx" @@ -29,9 +36,6 @@ type Props = { insertTopMargin?: boolean liturgicalChant: DisplayedActiveLiturgicalChant sortOrder: LiturgiesSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveLiturgicalChantsListItem: FC = props => { @@ -40,13 +44,12 @@ const ActiveLiturgicalChantsListItem: FC = props => { liturgicalChant: { dynamic: { value }, static: { id, check, check_penalty, traditions, improvement_cost, translations }, + maximum, + minimum, isDecreasable, isIncreasable, }, sortOrder, - addPoint, - removePoint, - remove, } = props const translate = useTranslate() @@ -60,6 +63,24 @@ const ActiveLiturgicalChantsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + id, + value, + maximum, + minimum ?? 0, + fromRaw(improvement_cost), + incrementLiturgicalChant, + decrementLiturgicalChant, + setLiturgicalChant, + removeLiturgicalChant, + ) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "LiturgicalChant", liturgical_chant: id })), [dispatch, id], @@ -98,19 +119,19 @@ const ActiveLiturgicalChantsListItem: FC = props => { } /> - + 0 ? handleSetToMinimumPoints : undefined} + decrementIsRemove={value === 0} selectForInfo={handleSelectForInfo} /> diff --git a/src/main_window/routes/characters/character/liturgicalChants/InactiveBlessingsListItem.tsx b/src/main_window/routes/characters/character/liturgicalChants/InactiveBlessingsListItem.tsx index ace17880..4f7348bd 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/InactiveBlessingsListItem.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/InactiveBlessingsListItem.tsx @@ -43,6 +43,10 @@ const InactiveBlessingsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const handleAdd = useCallback(() => { + add(id) + }, [add, id]) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Blessing", blessing: id })), [dispatch, id], @@ -68,7 +72,7 @@ const InactiveBlessingsListItem: FC = props => { - + ) } diff --git a/src/main_window/routes/characters/character/liturgicalChants/InactiveCeremoniesListItem.tsx b/src/main_window/routes/characters/character/liturgicalChants/InactiveCeremoniesListItem.tsx index 803ec8aa..672cceba 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/InactiveCeremoniesListItem.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/InactiveCeremoniesListItem.tsx @@ -11,8 +11,10 @@ import { LiturgiesSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { selectActiveBlessedTradition } from "../../../../selectors/traditionSelectors.ts" +import { addCeremony } from "../../../../slices/ceremoniesSlice.ts" import { selectStaticAspects } from "../../../../slices/databaseSlice.ts" import { changeInlineLibraryEntry, @@ -27,7 +29,6 @@ type Props = { insertTopMargin?: boolean ceremony: DisplayedInactiveCeremony sortOrder: LiturgiesSortOrder - add: (id: number) => void } const InactiveCeremoniesListItem: FC = props => { @@ -38,7 +39,6 @@ const InactiveCeremoniesListItem: FC = props => { isAvailable, }, sortOrder, - add, } = props const translate = useTranslate() @@ -51,6 +51,8 @@ const InactiveCeremoniesListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { handleAdd } = useInactiveActivatableActions(id, fromRaw(improvement_cost), addCeremony) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Ceremony", ceremony: id })), [dispatch, id], @@ -93,9 +95,7 @@ const InactiveCeremoniesListItem: FC = props => { diff --git a/src/main_window/routes/characters/character/liturgicalChants/InactiveLiturgicalChantsListItem.tsx b/src/main_window/routes/characters/character/liturgicalChants/InactiveLiturgicalChantsListItem.tsx index 246a4fce..aa521db7 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/InactiveLiturgicalChantsListItem.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/InactiveLiturgicalChantsListItem.tsx @@ -11,6 +11,7 @@ import { LiturgiesSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { selectActiveBlessedTradition } from "../../../../selectors/traditionSelectors.ts" import { selectStaticAspects } from "../../../../slices/databaseSlice.ts" @@ -18,6 +19,7 @@ import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { addLiturgicalChant } from "../../../../slices/liturgicalChantsSlice.ts" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillCheck } from "../skills/SkillCheck.tsx" import { SkillFill } from "../skills/SkillFill.tsx" @@ -27,7 +29,6 @@ type Props = { insertTopMargin?: boolean liturgicalChant: DisplayedInactiveLiturgicalChant sortOrder: LiturgiesSortOrder - add: (id: number) => void } const InactiveLiturgicalChantsListItem: FC = props => { @@ -38,7 +39,6 @@ const InactiveLiturgicalChantsListItem: FC = props => { isAvailable, }, sortOrder, - add, } = props const translate = useTranslate() @@ -51,6 +51,12 @@ const InactiveLiturgicalChantsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { handleAdd } = useInactiveActivatableActions( + id, + fromRaw(improvement_cost), + addLiturgicalChant, + ) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "LiturgicalChant", liturgical_chant: id })), [dispatch, id], @@ -96,9 +102,7 @@ const InactiveLiturgicalChantsListItem: FC = props => { diff --git a/src/main_window/routes/characters/character/liturgicalChants/LiturgicalChants.tsx b/src/main_window/routes/characters/character/liturgicalChants/LiturgicalChants.tsx index 2f49f395..15bf6b58 100644 --- a/src/main_window/routes/characters/character/liturgicalChants/LiturgicalChants.tsx +++ b/src/main_window/routes/characters/character/liturgicalChants/LiturgicalChants.tsx @@ -28,18 +28,6 @@ import { selectVisibleInactiveLiturgies, } from "../../../../selectors/liturgicalChantSelectors.ts" import { addBlessing, removeBlessing } from "../../../../slices/blessingsSlice.ts" -import { - addCeremony, - decrementCeremony, - incrementCeremony, - removeCeremony, -} from "../../../../slices/ceremoniesSlice.ts" -import { - addLiturgicalChant, - decrementLiturgicalChant, - incrementLiturgicalChant, - removeLiturgicalChant, -} from "../../../../slices/liturgicalChantsSlice.ts" import { changeLiturgiesSortOrder, selectLiturgiesSortOrder, @@ -103,44 +91,13 @@ export const LiturgicalChants: FC = () => { [visibleActiveLiturgies, inactiveFilterText, sortOrder, translateMap, localeCompare], ) - const handleAddLiturgicalChantPoint = useCallback( - (id: number) => dispatch(incrementLiturgicalChant(id)), + // TODO: Check AP + const handleAddBlessing = useCallback((id: number) => dispatch(addBlessing({ id })), [dispatch]) + const handleRemoveBlessing = useCallback( + (id: number) => dispatch(removeBlessing({ id })), [dispatch], ) - const handleRemoveLiturgicalChantPoint = useCallback( - (id: number) => dispatch(decrementLiturgicalChant(id)), - [dispatch], - ) - - const handleAddCeremonyPoint = useCallback( - (id: number) => dispatch(incrementCeremony(id)), - [dispatch], - ) - - const handleRemoveCeremonyPoint = useCallback( - (id: number) => dispatch(decrementCeremony(id)), - [dispatch], - ) - - const handleAddBlessing = useCallback((id: number) => dispatch(addBlessing(id)), [dispatch]) - - const handleAddLiturgicalChant = useCallback( - (id: number) => dispatch(addLiturgicalChant(id)), - [dispatch], - ) - - const handleAddCeremony = useCallback((id: number) => dispatch(addCeremony(id)), [dispatch]) - - const handleRemoveBlessing = useCallback((id: number) => dispatch(removeBlessing(id)), [dispatch]) - - const handleRemoveLiturgicalChant = useCallback( - (id: number) => dispatch(removeLiturgicalChant(id)), - [dispatch], - ) - - const handleRemoveCeremony = useCallback((id: number) => dispatch(removeCeremony(id)), [dispatch]) - const { isOpen: isSlideinVisible, open: openSlidein, close: closeSlidein } = useModalState() return ( @@ -177,29 +134,17 @@ export const LiturgicalChants: FC = () => {
- - {translate("liturgicalchants.header.name")} - + {translate("Name")} {translate("liturgicalchants.header.traditions")} - {sortOrder === LiturgiesSortOrder.Group - ? ` / ${translate("liturgicalchants.header.group")}` - : null} + {sortOrder === LiturgiesSortOrder.Group ? ` / ${translate("Group")}` : null} - - {translate("liturgicalchants.header.check")} + {translate("Check")} + + {translate("Mod")} - - {translate("liturgicalchants.header.checkmodifier")} - - - {translate("liturgicalchants.header.improvementcost")} + + {translate("IC")} @@ -226,7 +171,6 @@ export const LiturgicalChants: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} liturgicalChant={x} sortOrder={sortOrder} - add={handleAddLiturgicalChant} /> ) @@ -237,7 +181,6 @@ export const LiturgicalChants: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} ceremony={x} sortOrder={sortOrder} - add={handleAddCeremony} /> ) @@ -282,35 +225,20 @@ export const LiturgicalChants: FC = () => {
- - {translate("liturgicalchants.header.name")} - + {translate("Name")} {translate("liturgicalchants.header.traditions")} - {sortOrder === LiturgiesSortOrder.Group - ? ` / ${translate("liturgicalchants.header.group")}` - : null} + {sortOrder === LiturgiesSortOrder.Group ? ` / ${translate("Group")}` : null} - - {translate("liturgicalchants.header.skillrating")} + + {translate("SR")} - - {translate("liturgicalchants.header.check")} + {translate("Check")} + + {translate("Mod")} - - {translate("liturgicalchants.header.checkmodifier")} - - - {translate("liturgicalchants.header.improvementcost")} + + {translate("IC")} {canRemove ? : null} @@ -339,9 +267,6 @@ export const LiturgicalChants: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} liturgicalChant={x} sortOrder={sortOrder} - addPoint={handleAddLiturgicalChantPoint} - removePoint={handleRemoveLiturgicalChantPoint} - remove={handleRemoveLiturgicalChant} /> ) @@ -352,9 +277,6 @@ export const LiturgicalChants: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} ceremony={x} sortOrder={sortOrder} - addPoint={handleAddCeremonyPoint} - removePoint={handleRemoveCeremonyPoint} - remove={handleRemoveCeremony} /> ) diff --git a/src/main_window/routes/characters/character/skills/SkillButtons.tsx b/src/main_window/routes/characters/character/skills/SkillButtons.tsx index 9641543f..23a796b5 100644 --- a/src/main_window/routes/characters/character/skills/SkillButtons.tsx +++ b/src/main_window/routes/characters/character/skills/SkillButtons.tsx @@ -1,21 +1,20 @@ -import { useCallback } from "react" +import { MouseEvent, useCallback } from "react" import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx" import { ListItemButtons } from "../../../../../shared/components/list/ListItemButtons.tsx" -import { ImprovementCost } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" type Props = { activateDisabled?: boolean addDisabled?: boolean - ic?: ImprovementCost - id: number isNotActive?: boolean removeDisabled?: boolean - sr?: number - activate?(id: number): void - addPoint?(id: number): void - removePoint?(id: number): void - selectForInfo(id: number): void + decrementIsRemove?: boolean + activate?(): void + addPoint?(): void + setToMax?(): void + removePoint?(): void + setToMin?(): void + selectForInfo(): void } /** @@ -25,40 +24,43 @@ export const SkillButtons: React.FC = props => { const { activateDisabled, addDisabled, - ic, - id, isNotActive, removeDisabled, - sr, + decrementIsRemove, activate, addPoint, + setToMax, + setToMin, removePoint, selectForInfo, } = props const translate = useTranslate() - const boundSelectForInfo = useCallback(() => selectForInfo(id), [selectForInfo, id]) - - const getRemoveIcon = () => - (sr !== undefined && sr === 0 && removeDisabled !== true) || ic === undefined - ? "\uE90b" - : "\uE909" - - const handleActivation = useCallback( - () => (typeof activate === "function" ? activate(id) : undefined), - [activate, id], - ) - const handleAddPoint = useCallback( - () => (addDisabled !== true && typeof addPoint === "function" ? addPoint(id) : undefined), - [addPoint, id, addDisabled], + (event: MouseEvent) => { + if (addDisabled !== true && typeof addPoint === "function") { + if (event.shiftKey && typeof setToMax === "function") { + setToMax() + } else { + addPoint() + } + } + }, + [addDisabled, addPoint, setToMax], ) const handleRemovePoint = useCallback( - () => - removeDisabled !== true && typeof removePoint === "function" ? removePoint(id) : undefined, - [removePoint, id, removeDisabled], + (event: MouseEvent) => { + if (removeDisabled !== true && typeof removePoint === "function") { + if (event.shiftKey && typeof setToMin === "function") { + setToMin() + } else { + removePoint() + } + } + }, + [removeDisabled, removePoint, setToMin], ) return ( @@ -66,7 +68,7 @@ export const SkillButtons: React.FC = props => { {isNotActive === true ? ( = props => { ) : null} {typeof removePoint === "function" ? ( = props => { ) : null} )} - + ) } diff --git a/src/main_window/routes/characters/character/skills/SkillGroup.tsx b/src/main_window/routes/characters/character/skills/SkillGroup.tsx deleted file mode 100644 index 69676c2f..00000000 --- a/src/main_window/routes/characters/character/skills/SkillGroup.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { FC } from "react" -import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx" - -type Props = { - addText?: string - group?: number - getGroupName?: (id: number) => string -} - -/** - * Returns a row section that display a skill group. - */ -export const SkillGroup: FC = props => { - const { addText, group, getGroupName } = props - - if (addText === undefined && (group === undefined || getGroupName === undefined)) { - return null - } - - return -} diff --git a/src/main_window/routes/characters/character/skills/SkillListItem.tsx b/src/main_window/routes/characters/character/skills/SkillListItem.tsx index abadc1b6..e7641cdb 100644 --- a/src/main_window/routes/characters/character/skills/SkillListItem.tsx +++ b/src/main_window/routes/characters/character/skills/SkillListItem.tsx @@ -1,125 +1,100 @@ -import { - SkillCheckPenalty, - SkillCheck as SkillCheckType, -} from "optolith-database-schema/types/_SkillCheck" -import { memo, useCallback } from "react" +import { memo, useCallback, useMemo } from "react" import { ListItem } from "../../../../../shared/components/list/ListItem.tsx" +import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx" import { ListItemName } from "../../../../../shared/components/list/ListItemName.tsx" import { ListItemSeparator } from "../../../../../shared/components/list/ListItemSeparator.tsx" import { ListItemValues } from "../../../../../shared/components/list/ListItemValues.tsx" -import { ImprovementCost } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useRatedActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" +import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" +import { DisplayedSkill } from "../../../../selectors/skillsSelectors.ts" import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" -import { AdditionalValue, SkillAdditionalValues } from "./SkillAdditionalValues.tsx" +import { selectSkillsCultureRatingVisibility } from "../../../../slices/settingsSlice.ts" +import { decrementSkill, incrementSkill, setSkill } from "../../../../slices/skillsSlice.ts" import { SkillButtons } from "./SkillButtons.tsx" import { SkillCheck } from "./SkillCheck.tsx" -import { SkillFill } from "./SkillFill.tsx" -import { SkillGroup } from "./SkillGroup.tsx" import { SkillImprovementCost } from "./SkillImprovementCost.tsx" import { SkillRating } from "./SkillRating.tsx" type Props = { - activateDisabled?: boolean - addDisabled?: boolean - addFillElement?: boolean - addValues?: AdditionalValue[] - addText?: string - check?: SkillCheckType - checkDisabled?: boolean - checkmod?: SkillCheckPenalty - group?: number - ic?: ImprovementCost - id: number insertTopMargin?: boolean - isNotActive?: boolean - name: string - noIncrease?: boolean - removeDisabled?: boolean - sr?: number - typ?: boolean - untyp?: boolean - activate?(id: number): void - addPoint?(id: number): void - removePoint?(id: number): void - getGroupName?: (id: number) => string + skill: DisplayedSkill } const SkillListItem: React.FC = props => { const { - activateDisabled, - addDisabled, - addFillElement, - addValues, - addText, - check, - checkDisabled, - checkmod, - getGroupName, - group, - ic, - id, insertTopMargin, - isNotActive, - name, - noIncrease, - removeDisabled, - sr, - typ, - untyp, - activate, - addPoint, - removePoint, + skill: { + static: { id, check, group, improvement_cost, translations }, + dynamic: { value }, + maximum, + minimum, + isIncreasable, + isDecreasable, + commonness, + }, } = props + const translateMap = useTranslateMap() const dispatch = useAppDispatch() const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId) const canRemove = useAppSelector(selectCanRemove) + const cultureRatingVisibility = useAppSelector(selectSkillsCultureRatingVisibility) + + const { name = "???" } = translateMap(translations) ?? {} + + const { handleAddPoint, handleRemovePoint, handleSetToMaximumPoints, handleSetToMinimumPoints } = + useRatedActions( + id, + value, + maximum, + minimum, + fromRaw(improvement_cost), + incrementSkill, + decrementSkill, + setSkill, + ) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Skill", skill: id })), [dispatch, id], ) + const getStaticSkillGroupById = useAppSelector(SelectGetById.Static.SkillGroup) + const groupName = useMemo( + () => translateMap(getStaticSkillGroupById(group.id.skill_group)?.translations)?.name, + [getStaticSkillGroupById, group.id.skill_group, translateMap], + ) + return ( - + - - {checkDisabled !== true && check !== undefined ? ( - - ) : null} - {addFillElement === true ? : null} - - + + + diff --git a/src/main_window/routes/characters/character/skills/SkillRating.tsx b/src/main_window/routes/characters/character/skills/SkillRating.tsx index dfd86aa5..45924250 100644 --- a/src/main_window/routes/characters/character/skills/SkillRating.tsx +++ b/src/main_window/routes/characters/character/skills/SkillRating.tsx @@ -4,18 +4,17 @@ type Props = { isNotActive?: boolean noIncrease?: boolean sr?: number - addPoint?(id: number): void } /** * Returns a row section that displays a skill rating. */ export const SkillRating: FC = props => { - const { isNotActive, noIncrease, sr, addPoint } = props + const { isNotActive, noIncrease, sr } = props if (typeof sr === "number") { return
{sr}
- } else if (addPoint === undefined && isNotActive !== true && noIncrease !== true) { + } else if (isNotActive !== true && noIncrease !== true) { return
} diff --git a/src/main_window/routes/characters/character/skills/Skills.tsx b/src/main_window/routes/characters/character/skills/Skills.tsx index fd8b4f15..06a2cdf3 100644 --- a/src/main_window/routes/characters/character/skills/Skills.tsx +++ b/src/main_window/routes/characters/character/skills/Skills.tsx @@ -26,14 +26,12 @@ import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { InlineLibrary } from "../../../../inlineLibrary/InlineLibrary.tsx" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" import { DisplayedSkill, selectVisibleSkills } from "../../../../selectors/skillsSelectors.ts" -import { selectStaticSkillGroups } from "../../../../slices/databaseSlice.ts" import { changeSkillsSortOrder, selectSkillsCultureRatingVisibility, selectSkillsSortOrder, switchSkillsCultureRatingVisibility, } from "../../../../slices/settingsSlice.ts" -import { decrementSkill, incrementSkill } from "../../../../slices/skillsSlice.ts" import { SkillListItem } from "./SkillListItem.tsx" import "./Skills.scss" @@ -62,7 +60,6 @@ export const Skills: FC = () => { ) const canRemove = useAppSelector(selectCanRemove) - const skillGroups = useAppSelector(selectStaticSkillGroups) const [filterText, setFilterText] = useState("") const sortOrder = useAppSelector(selectSkillsSortOrder) @@ -70,7 +67,9 @@ export const Skills: FC = () => { (id: SkillsSortOrder) => dispatch(changeSkillsSortOrder(id)), [dispatch], ) + const visibleSkills = useAppSelector(selectVisibleSkills) + const list = useMemo( () => visibleSkills @@ -106,15 +105,6 @@ export const Skills: FC = () => { [filterText, localeCompare, sortOrder, translateMap, visibleSkills], ) - const handleAdd = useCallback((id: number) => dispatch(incrementSkill(id)), [dispatch]) - - const handleRemove = useCallback((id: number) => dispatch(decrementSkill(id)), [dispatch]) - - const getGroupName = useCallback( - (id: number) => translateMap(skillGroups[id]?.translations)?.name ?? "", - [skillGroups, translateMap], - ) - return ( @@ -140,21 +130,21 @@ export const Skills: FC = () => { /> - {translate("skills.commonskills")} + {translate("Common Skills")} {cultureRatingVisibility ? : null}
- {translate("skills.header.name")} - {translate("skills.header.group")} - - {translate("skills.header.skillrating")} + {translate("Name")} + {translate("Group")} + + {translate("SR")} - {translate("skills.header.check")} - - {translate("skills.header.improvementcost")} + {translate("Check")} + + {translate("IC")} {canRemove ? : null} @@ -163,29 +153,13 @@ export const Skills: FC = () => { {list.length > 0 ? ( - {list.map((x, i) => { - const translation = translateMap(x.static.translations) - return ( - - ) - })} + {list.map((x, i) => ( + + ))} ) : ( diff --git a/src/main_window/routes/characters/character/spells/ActiveAnimistPowersListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveAnimistPowersListItem.tsx index bfc51827..53230efa 100644 --- a/src/main_window/routes/characters/character/spells/ActiveAnimistPowersListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveAnimistPowersListItem.tsx @@ -1,28 +1,45 @@ import { FC, memo } from "react" -import { - ImprovementCost, - fromRaw, -} from "../../../../../shared/domain/adventurePoints/improvementCost.ts" import { DisplayedActiveAnimistPower } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" -import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementAnimistPower, + incrementAnimistPower, + removeAnimistPower, + setAnimistPower, +} from "../../../../slices/magicalActions/animistPowersSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean animistPower: DisplayedActiveAnimistPower sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveAnimistPowersListItem: FC = props => { - const { insertTopMargin, animistPower, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, animistPower, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + animistPower.static.id, + animistPower.dynamic.value, + animistPower.maximum, + animistPower.minimum ?? 0, + animistPower.improvementCost, + incrementAnimistPower, + decrementAnimistPower, + setAnimistPower, + removeAnimistPower, + ) + return ( = props => { magicalAction={animistPower} sortOrder={sortOrder} groupName={translate("Animist Powers")} - improvementCost={(() => { - switch (animistPower.static.improvement_cost.tag) { - case "Fixed": - return fromRaw(animistPower.static.improvement_cost.fixed) - case "ByPrimaryPatron": - // TODO: Replace with derived improvement cost - return ImprovementCost.D - default: - return assertExhaustive(animistPower.static.improvement_cost) - } - })()} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + improvementCost={animistPower.improvementCost} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveCantripsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveCantripsListItem.tsx index ad2c91a8..cde1b846 100644 --- a/src/main_window/routes/characters/character/spells/ActiveCantripsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveCantripsListItem.tsx @@ -45,6 +45,10 @@ const ActiveCantripsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const handleRemove = useCallback(() => { + remove(id) + }, [remove, id]) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Cantrip", cantrip: id })), [dispatch, id], @@ -75,8 +79,7 @@ const ActiveCantripsListItem: FC = props => { diff --git a/src/main_window/routes/characters/character/spells/ActiveCursesListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveCursesListItem.tsx index ac0a4eec..74c8d765 100644 --- a/src/main_window/routes/characters/character/spells/ActiveCursesListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveCursesListItem.tsx @@ -3,22 +3,44 @@ import { cursesImprovementCost } from "../../../../../shared/domain/rated/magica import { DisplayedActiveCurse } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementCurse, + incrementCurse, + removeCurse, + setCurse, +} from "../../../../slices/magicalActions/cursesSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean curse: DisplayedActiveCurse sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveCursesListItem: FC = props => { - const { insertTopMargin, curse, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, curse, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + curse.static.id, + curse.dynamic.value, + curse.maximum, + curse.minimum ?? 0, + cursesImprovementCost, + incrementCurse, + decrementCurse, + setCurse, + removeCurse, + ) + return ( = props => { groupName={translate("Curses")} checkPenalty={curse.static.check_penalty} improvementCost={cursesImprovementCost} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveDominationRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveDominationRitualsListItem.tsx index fdc16bc8..440fa5f6 100644 --- a/src/main_window/routes/characters/character/spells/ActiveDominationRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveDominationRitualsListItem.tsx @@ -3,22 +3,44 @@ import { dominationRitualsImprovementCost } from "../../../../../shared/domain/r import { DisplayedActiveDominationRitual } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementDominationRitual, + incrementDominationRitual, + removeDominationRitual, + setDominationRitual, +} from "../../../../slices/magicalActions/dominationRitualsSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean dominationRitual: DisplayedActiveDominationRitual sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveDominationRitualsListItem: FC = props => { - const { insertTopMargin, dominationRitual, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, dominationRitual, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + dominationRitual.static.id, + dominationRitual.dynamic.value, + dominationRitual.maximum, + dominationRitual.minimum ?? 0, + dominationRitualsImprovementCost, + incrementDominationRitual, + decrementDominationRitual, + setDominationRitual, + removeDominationRitual, + ) + return ( = props => { groupName={translate("Domination Rituals")} checkPenalty={dominationRitual.static.check_penalty} improvementCost={dominationRitualsImprovementCost} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveElvenMagicalSongsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveElvenMagicalSongsListItem.tsx index b5175dba..07f7eae1 100644 --- a/src/main_window/routes/characters/character/spells/ActiveElvenMagicalSongsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveElvenMagicalSongsListItem.tsx @@ -3,22 +3,44 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedActiveElvenMagicalSong } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementElvenMagicalSong, + incrementElvenMagicalSong, + removeElvenMagicalSong, + setElvenMagicalSong, +} from "../../../../slices/magicalActions/elvenMagicalSongsSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean elvenMagicalSong: DisplayedActiveElvenMagicalSong sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveElvenMagicalSongsListItem: FC = props => { - const { insertTopMargin, elvenMagicalSong, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, elvenMagicalSong, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + elvenMagicalSong.static.id, + elvenMagicalSong.dynamic.value, + elvenMagicalSong.maximum, + elvenMagicalSong.minimum ?? 0, + fromRaw(elvenMagicalSong.static.improvement_cost), + incrementElvenMagicalSong, + decrementElvenMagicalSong, + setElvenMagicalSong, + removeElvenMagicalSong, + ) + return ( = props => { groupName={translate("Elven Magical Songs")} checkPenalty={elvenMagicalSong.static.check_penalty} improvementCost={fromRaw(elvenMagicalSong.static.improvement_cost)} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveGeodeRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveGeodeRitualsListItem.tsx index ed328923..c9ee6c8b 100644 --- a/src/main_window/routes/characters/character/spells/ActiveGeodeRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveGeodeRitualsListItem.tsx @@ -3,22 +3,44 @@ import { geodeRitualsImprovementCost } from "../../../../../shared/domain/rated/ import { DisplayedActiveGeodeRitual } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementGeodeRitual, + incrementGeodeRitual, + removeGeodeRitual, + setGeodeRitual, +} from "../../../../slices/magicalActions/geodeRitualsSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean geodeRitual: DisplayedActiveGeodeRitual sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveGeodeRitualsListItem: FC = props => { - const { insertTopMargin, geodeRitual, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, geodeRitual, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + geodeRitual.static.id, + geodeRitual.dynamic.value, + geodeRitual.maximum, + geodeRitual.minimum ?? 0, + geodeRitualsImprovementCost, + incrementGeodeRitual, + decrementGeodeRitual, + setGeodeRitual, + removeGeodeRitual, + ) + return ( = props => { sortOrder={sortOrder} groupName={translate("Geode Rituals")} improvementCost={geodeRitualsImprovementCost} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveJesterTricksListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveJesterTricksListItem.tsx index 8654ea94..13ee09b7 100644 --- a/src/main_window/routes/characters/character/spells/ActiveJesterTricksListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveJesterTricksListItem.tsx @@ -3,22 +3,44 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedActiveJesterTrick } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementJesterTrick, + incrementJesterTrick, + removeJesterTrick, + setJesterTrick, +} from "../../../../slices/magicalActions/jesterTricksSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean jesterTrick: DisplayedActiveJesterTrick sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveJesterTricksListItem: FC = props => { - const { insertTopMargin, jesterTrick, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, jesterTrick, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + jesterTrick.static.id, + jesterTrick.dynamic.value, + jesterTrick.maximum, + jesterTrick.minimum ?? 0, + fromRaw(jesterTrick.static.improvement_cost), + incrementJesterTrick, + decrementJesterTrick, + setJesterTrick, + removeJesterTrick, + ) + return ( = props => { groupName={translate("Jester Tricks")} checkPenalty={jesterTrick.static.check_penalty} improvementCost={fromRaw(jesterTrick.static.improvement_cost)} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveMagicalActionsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveMagicalActionsListItem.tsx index fa0ce902..be94ce17 100644 --- a/src/main_window/routes/characters/character/spells/ActiveMagicalActionsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveMagicalActionsListItem.tsx @@ -42,10 +42,12 @@ type Props = { sortOrder: SpellsSortOrder groupName: string checkPenalty?: SkillCheckPenalty - improvementCost?: ImprovementCost - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void + improvementCost: ImprovementCost + addPoint: () => void + removePoint: () => void + setToMaximumPoints: () => void + setToMinimumPoints: () => void + remove: () => void } /** @@ -64,14 +66,16 @@ export const ActiveMagicalActionsListItem = - + 0 ? setToMinimumPoints : undefined} + decrementIsRemove={value === 0} selectForInfo={handleSelectForInfo} /> diff --git a/src/main_window/routes/characters/character/spells/ActiveMagicalDancesListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveMagicalDancesListItem.tsx index 946cc4ef..384f8b89 100644 --- a/src/main_window/routes/characters/character/spells/ActiveMagicalDancesListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveMagicalDancesListItem.tsx @@ -3,22 +3,44 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedActiveMagicalDance } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementMagicalDance, + incrementMagicalDance, + removeMagicalDance, + setMagicalDance, +} from "../../../../slices/magicalActions/magicalDancesSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean magicalDance: DisplayedActiveMagicalDance sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveMagicalDancesListItem: FC = props => { - const { insertTopMargin, magicalDance, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, magicalDance, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + magicalDance.static.id, + magicalDance.dynamic.value, + magicalDance.maximum, + magicalDance.minimum ?? 0, + fromRaw(magicalDance.static.improvement_cost), + incrementMagicalDance, + decrementMagicalDance, + setMagicalDance, + removeMagicalDance, + ) + return ( = props => { sortOrder={sortOrder} groupName={translate("Magical Dances")} improvementCost={fromRaw(magicalDance.static.improvement_cost)} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveMagicalMelodiesListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveMagicalMelodiesListItem.tsx index 2644f860..b797c5d6 100644 --- a/src/main_window/routes/characters/character/spells/ActiveMagicalMelodiesListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveMagicalMelodiesListItem.tsx @@ -3,22 +3,44 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedActiveMagicalMelody } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementMagicalMelody, + incrementMagicalMelody, + removeMagicalMelody, + setMagicalMelody, +} from "../../../../slices/magicalActions/magicalMelodiesSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean magicalMelody: DisplayedActiveMagicalMelody sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveMagicalMelodiesListItem: FC = props => { - const { insertTopMargin, magicalMelody, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, magicalMelody, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + magicalMelody.static.id, + magicalMelody.dynamic.value, + magicalMelody.maximum, + magicalMelody.minimum ?? 0, + fromRaw(magicalMelody.static.improvement_cost), + incrementMagicalMelody, + decrementMagicalMelody, + setMagicalMelody, + removeMagicalMelody, + ) + return ( = props => { groupName={translate("Magical Melodies")} checkPenalty={magicalMelody.static.check_penalty} improvementCost={fromRaw(magicalMelody.static.improvement_cost)} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/ActiveRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveRitualsListItem.tsx index 3d0f8a70..1097bdb1 100644 --- a/src/main_window/routes/characters/character/spells/ActiveRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveRitualsListItem.tsx @@ -9,6 +9,7 @@ import { DisplayedActiveRitual } from "../../../../../shared/domain/rated/spellA import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" @@ -16,6 +17,12 @@ import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { + decrementRitual, + incrementRitual, + removeRitual, + setRitual, +} from "../../../../slices/ritualsSlice.ts" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillCheck } from "../skills/SkillCheck.tsx" import { SkillFill } from "../skills/SkillFill.tsx" @@ -26,9 +33,6 @@ type Props = { insertTopMargin?: boolean ritual: DisplayedActiveRitual sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveRitualsListItem: FC = props => { @@ -37,14 +41,13 @@ const ActiveRitualsListItem: FC = props => { ritual: { dynamic: { value }, static: { id, check, check_penalty, property, improvement_cost, translations }, + maximum, + minimum, isDecreasable, isIncreasable, isUnfamiliar, }, sortOrder, - addPoint, - removePoint, - remove, } = props const translate = useTranslate() @@ -56,6 +59,24 @@ const ActiveRitualsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + id, + value, + maximum, + minimum ?? 0, + fromRaw(improvement_cost), + incrementRitual, + decrementRitual, + setRitual, + removeRitual, + ) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Ritual", ritual: id })), [dispatch, id], @@ -82,19 +103,19 @@ const ActiveRitualsListItem: FC = props => { } /> - + 0 ? handleSetToMinimumPoints : undefined} + decrementIsRemove={value === 0} selectForInfo={handleSelectForInfo} /> diff --git a/src/main_window/routes/characters/character/spells/ActiveSpellsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveSpellsListItem.tsx index 1e29a64a..b1b2c689 100644 --- a/src/main_window/routes/characters/character/spells/ActiveSpellsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveSpellsListItem.tsx @@ -9,6 +9,7 @@ import { DisplayedActiveSpell } from "../../../../../shared/domain/rated/spellAc import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" @@ -16,6 +17,12 @@ import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { + decrementSpell, + incrementSpell, + removeSpell, + setSpell, +} from "../../../../slices/spellsSlice.ts" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillCheck } from "../skills/SkillCheck.tsx" import { SkillFill } from "../skills/SkillFill.tsx" @@ -26,9 +33,6 @@ type Props = { insertTopMargin?: boolean spell: DisplayedActiveSpell sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveSpellsListItem: FC = props => { @@ -37,14 +41,13 @@ const ActiveSpellsListItem: FC = props => { spell: { dynamic: { value }, static: { id, check, check_penalty, property, improvement_cost, translations }, + maximum, + minimum, isDecreasable, isIncreasable, isUnfamiliar, }, sortOrder, - addPoint, - removePoint, - remove, } = props const translate = useTranslate() @@ -54,7 +57,25 @@ const ActiveSpellsListItem: FC = props => { const canRemove = useAppSelector(selectCanRemove) const getProperty = useAppSelector(SelectGetById.Static.Property) - const { name = "" } = translateMap(translations) ?? {} + const { name = "???" } = translateMap(translations) ?? {} + + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + id, + value, + maximum, + minimum ?? 0, + fromRaw(improvement_cost), + incrementSpell, + decrementSpell, + setSpell, + removeSpell, + ) const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Spell", spell: id })), @@ -82,19 +103,19 @@ const ActiveSpellsListItem: FC = props => { } /> - + 0 ? handleSetToMinimumPoints : undefined} + decrementIsRemove={value === 0} selectForInfo={handleSelectForInfo} /> diff --git a/src/main_window/routes/characters/character/spells/ActiveZibiljaRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/ActiveZibiljaRitualsListItem.tsx index 528343a7..eb467e26 100644 --- a/src/main_window/routes/characters/character/spells/ActiveZibiljaRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/ActiveZibiljaRitualsListItem.tsx @@ -3,22 +3,44 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedActiveZibiljaRitual } from "../../../../../shared/domain/rated/spellActive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useActiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { + decrementZibiljaRitual, + incrementZibiljaRitual, + removeZibiljaRitual, + setZibiljaRitual, +} from "../../../../slices/magicalActions/zibiljaRitualsSlice.ts" import { ActiveMagicalActionsListItem } from "./ActiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean zibiljaRitual: DisplayedActiveZibiljaRitual sortOrder: SpellsSortOrder - addPoint: (id: number) => void - removePoint: (id: number) => void - remove: (id: number) => void } const ActiveZibiljaRitualsListItem: FC = props => { - const { insertTopMargin, zibiljaRitual, sortOrder, addPoint, removePoint, remove } = props + const { insertTopMargin, zibiljaRitual, sortOrder } = props const translate = useTranslate() + const { + handleAddPoint, + handleRemovePoint, + handleSetToMaximumPoints, + handleSetToMinimumPoints, + handleRemove, + } = useActiveActivatableActions( + zibiljaRitual.static.id, + zibiljaRitual.dynamic.value, + zibiljaRitual.maximum, + zibiljaRitual.minimum ?? 0, + fromRaw(zibiljaRitual.static.improvement_cost), + incrementZibiljaRitual, + decrementZibiljaRitual, + setZibiljaRitual, + removeZibiljaRitual, + ) + return ( = props => { groupName={translate("Zibilja Rituals")} checkPenalty={zibiljaRitual.static.check_penalty} improvementCost={fromRaw(zibiljaRitual.static.improvement_cost)} - addPoint={addPoint} - removePoint={removePoint} - remove={remove} + addPoint={handleAddPoint} + removePoint={handleRemovePoint} + setToMaximumPoints={handleSetToMaximumPoints} + setToMinimumPoints={handleSetToMinimumPoints} + remove={handleRemove} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveAnimistPowersListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveAnimistPowersListItem.tsx index f34ce2e7..840a0249 100644 --- a/src/main_window/routes/characters/character/spells/InactiveAnimistPowersListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveAnimistPowersListItem.tsx @@ -1,26 +1,28 @@ import { FC, memo } from "react" -import { - ImprovementCost, - fromRaw, -} from "../../../../../shared/domain/adventurePoints/improvementCost.ts" import { DisplayedInactiveAnimistPower } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" -import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addAnimistPower } from "../../../../slices/magicalActions/animistPowersSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean animistPower: DisplayedInactiveAnimistPower sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveAnimistPowersListItem: FC = props => { - const { insertTopMargin, animistPower, sortOrder, add } = props + const { insertTopMargin, animistPower, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + animistPower.static.id, + animistPower.improvementCost, + addAnimistPower, + ) + return ( = props => { magicalAction={animistPower} sortOrder={sortOrder} groupName={translate("Animist Powers")} - improvementCost={(() => { - switch (animistPower.static.improvement_cost.tag) { - case "Fixed": - return fromRaw(animistPower.static.improvement_cost.fixed) - case "ByPrimaryPatron": - // TODO: Replace with derived improvement cost - return ImprovementCost.D - default: - return assertExhaustive(animistPower.static.improvement_cost) - } - })()} - add={add} + improvementCost={animistPower.improvementCost} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveCantripsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveCantripsListItem.tsx index 1a4ca9ff..7e117a78 100644 --- a/src/main_window/routes/characters/character/spells/InactiveCantripsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveCantripsListItem.tsx @@ -43,6 +43,10 @@ const InactiveCantripsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const handleAdd = useCallback(() => { + add(id) + }, [add, id]) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Cantrip", cantrip: id })), [dispatch, id], @@ -72,7 +76,7 @@ const InactiveCantripsListItem: FC = props => { - + ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveCursesListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveCursesListItem.tsx index b063f153..b430f827 100644 --- a/src/main_window/routes/characters/character/spells/InactiveCursesListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveCursesListItem.tsx @@ -3,20 +3,27 @@ import { cursesImprovementCost } from "../../../../../shared/domain/rated/magica import { DisplayedInactiveCurse } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addCurse } from "../../../../slices/magicalActions/cursesSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean curse: DisplayedInactiveCurse sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveCursesListItem: FC = props => { - const { insertTopMargin, curse, sortOrder, add } = props + const { insertTopMargin, curse, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + curse.static.id, + cursesImprovementCost, + addCurse, + ) + return ( = props => { groupName={translate("Curses")} checkPenalty={curse.static.check_penalty} improvementCost={cursesImprovementCost} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveDominationRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveDominationRitualsListItem.tsx index cdd5b9ad..2d7f1627 100644 --- a/src/main_window/routes/characters/character/spells/InactiveDominationRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveDominationRitualsListItem.tsx @@ -3,20 +3,27 @@ import { dominationRitualsImprovementCost } from "../../../../../shared/domain/r import { DisplayedInactiveDominationRitual } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addDominationRitual } from "../../../../slices/magicalActions/dominationRitualsSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean dominationRitual: DisplayedInactiveDominationRitual sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveDominationRitualsListItem: FC = props => { - const { insertTopMargin, dominationRitual, sortOrder, add } = props + const { insertTopMargin, dominationRitual, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + dominationRitual.static.id, + dominationRitualsImprovementCost, + addDominationRitual, + ) + return ( = props => { groupName={translate("Domination Rituals")} checkPenalty={dominationRitual.static.check_penalty} improvementCost={dominationRitualsImprovementCost} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveElvenMagicalSongsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveElvenMagicalSongsListItem.tsx index 43496cc5..4c680c01 100644 --- a/src/main_window/routes/characters/character/spells/InactiveElvenMagicalSongsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveElvenMagicalSongsListItem.tsx @@ -3,20 +3,27 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedInactiveElvenMagicalSong } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addElvenMagicalSong } from "../../../../slices/magicalActions/elvenMagicalSongsSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean elvenMagicalSong: DisplayedInactiveElvenMagicalSong sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveElvenMagicalSongsListItem: FC = props => { - const { insertTopMargin, elvenMagicalSong, sortOrder, add } = props + const { insertTopMargin, elvenMagicalSong, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + elvenMagicalSong.static.id, + fromRaw(elvenMagicalSong.static.improvement_cost), + addElvenMagicalSong, + ) + return ( = props => { groupName={translate("Elven Magical Songs")} checkPenalty={elvenMagicalSong.static.check_penalty} improvementCost={fromRaw(elvenMagicalSong.static.improvement_cost)} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveGeodeRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveGeodeRitualsListItem.tsx index 5796e810..bdbcaed4 100644 --- a/src/main_window/routes/characters/character/spells/InactiveGeodeRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveGeodeRitualsListItem.tsx @@ -3,20 +3,27 @@ import { geodeRitualsImprovementCost } from "../../../../../shared/domain/rated/ import { DisplayedInactiveGeodeRitual } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addGeodeRitual } from "../../../../slices/magicalActions/geodeRitualsSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean geodeRitual: DisplayedInactiveGeodeRitual sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveGeodeRitualsListItem: FC = props => { - const { insertTopMargin, geodeRitual, sortOrder, add } = props + const { insertTopMargin, geodeRitual, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + geodeRitual.static.id, + geodeRitualsImprovementCost, + addGeodeRitual, + ) + return ( = props => { sortOrder={sortOrder} groupName={translate("Geode Rituals")} improvementCost={geodeRitualsImprovementCost} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveJesterTricksListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveJesterTricksListItem.tsx index 074f6a91..a4e26fcc 100644 --- a/src/main_window/routes/characters/character/spells/InactiveJesterTricksListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveJesterTricksListItem.tsx @@ -3,20 +3,27 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedInactiveJesterTrick } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addJesterTrick } from "../../../../slices/magicalActions/jesterTricksSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean jesterTrick: DisplayedInactiveJesterTrick sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveJesterTricksListItem: FC = props => { - const { insertTopMargin, jesterTrick, sortOrder, add } = props + const { insertTopMargin, jesterTrick, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + jesterTrick.static.id, + fromRaw(jesterTrick.static.improvement_cost), + addJesterTrick, + ) + return ( = props => { groupName={translate("Jester Tricks")} checkPenalty={jesterTrick.static.check_penalty} improvementCost={fromRaw(jesterTrick.static.improvement_cost)} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveMagicalActionsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveMagicalActionsListItem.tsx index af1b36c9..b5e86f7a 100644 --- a/src/main_window/routes/characters/character/spells/InactiveMagicalActionsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveMagicalActionsListItem.tsx @@ -41,7 +41,7 @@ type Props = { groupName: string checkPenalty?: SkillCheckPenalty improvementCost?: ImprovementCost - add: (id: number) => void + add: () => void } /** @@ -101,13 +101,7 @@ export const InactiveMagicalActionsListItem = - + ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveMagicalDancesListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveMagicalDancesListItem.tsx index df608abf..72ccf7ee 100644 --- a/src/main_window/routes/characters/character/spells/InactiveMagicalDancesListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveMagicalDancesListItem.tsx @@ -3,20 +3,27 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedInactiveMagicalDance } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addMagicalDance } from "../../../../slices/magicalActions/magicalDancesSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean magicalDance: DisplayedInactiveMagicalDance sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveMagicalDancesListItem: FC = props => { - const { insertTopMargin, magicalDance, sortOrder, add } = props + const { insertTopMargin, magicalDance, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + magicalDance.static.id, + fromRaw(magicalDance.static.improvement_cost), + addMagicalDance, + ) + return ( = props => { sortOrder={sortOrder} groupName={translate("Magical Dances")} improvementCost={fromRaw(magicalDance.static.improvement_cost)} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveMagicalMelodiesListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveMagicalMelodiesListItem.tsx index 7e3b5346..0ec24f37 100644 --- a/src/main_window/routes/characters/character/spells/InactiveMagicalMelodiesListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveMagicalMelodiesListItem.tsx @@ -3,20 +3,27 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedInactiveMagicalMelody } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addMagicalMelody } from "../../../../slices/magicalActions/magicalMelodiesSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean magicalMelody: DisplayedInactiveMagicalMelody sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveMagicalMelodiesListItem: FC = props => { - const { insertTopMargin, magicalMelody, sortOrder, add } = props + const { insertTopMargin, magicalMelody, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + magicalMelody.static.id, + fromRaw(magicalMelody.static.improvement_cost), + addMagicalMelody, + ) + return ( = props => { groupName={translate("Magical Melodies")} checkPenalty={magicalMelody.static.check_penalty} improvementCost={fromRaw(magicalMelody.static.improvement_cost)} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/InactiveRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveRitualsListItem.tsx index a08a2bc6..a5ee2f0d 100644 --- a/src/main_window/routes/characters/character/spells/InactiveRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveRitualsListItem.tsx @@ -9,12 +9,14 @@ import { DisplayedInactiveRitual } from "../../../../../shared/domain/rated/spel import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts" import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { addRitual } from "../../../../slices/ritualsSlice.ts" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillCheck } from "../skills/SkillCheck.tsx" import { SkillFill } from "../skills/SkillFill.tsx" @@ -24,7 +26,6 @@ type Props = { insertTopMargin?: boolean ritual: DisplayedInactiveRitual sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveRitualsListItem: FC = props => { @@ -36,7 +37,6 @@ const InactiveRitualsListItem: FC = props => { isUnfamiliar, }, sortOrder, - add, } = props const translate = useTranslate() @@ -47,6 +47,8 @@ const InactiveRitualsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { handleAdd } = useInactiveActivatableActions(id, fromRaw(improvement_cost), addRitual) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Ritual", ritual: id })), [dispatch, id], @@ -62,6 +64,7 @@ const InactiveRitualsListItem: FC = props => { insertTopMargin={insertTopMargin} active={inlineLibraryEntryId?.tag === "Ritual" && inlineLibraryEntryId.ritual === id} unrecommended={isUnfamiliar} + disabled={!isAvailable} > @@ -79,9 +82,7 @@ const InactiveRitualsListItem: FC = props => { diff --git a/src/main_window/routes/characters/character/spells/InactiveSpellsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveSpellsListItem.tsx index 22973e9f..d69a065b 100644 --- a/src/main_window/routes/characters/character/spells/InactiveSpellsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveSpellsListItem.tsx @@ -9,12 +9,14 @@ import { DisplayedInactiveSpell } from "../../../../../shared/domain/rated/spell import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts" import { changeInlineLibraryEntry, selectInlineLibraryEntryId, } from "../../../../slices/inlineWikiSlice.ts" +import { addSpell } from "../../../../slices/spellsSlice.ts" import { SkillButtons } from "../skills/SkillButtons.tsx" import { SkillCheck } from "../skills/SkillCheck.tsx" import { SkillFill } from "../skills/SkillFill.tsx" @@ -24,7 +26,6 @@ type Props = { insertTopMargin?: boolean spell: DisplayedInactiveSpell sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveSpellsListItem: FC = props => { @@ -36,7 +37,6 @@ const InactiveSpellsListItem: FC = props => { isUnfamiliar, }, sortOrder, - add, } = props const translate = useTranslate() @@ -47,6 +47,8 @@ const InactiveSpellsListItem: FC = props => { const { name = "" } = translateMap(translations) ?? {} + const { handleAdd } = useInactiveActivatableActions(id, fromRaw(improvement_cost), addSpell) + const handleSelectForInfo = useCallback( () => dispatch(changeInlineLibraryEntry({ tag: "Spell", spell: id })), [dispatch, id], @@ -62,6 +64,7 @@ const InactiveSpellsListItem: FC = props => { insertTopMargin={insertTopMargin} active={inlineLibraryEntryId?.tag === "Spell" && inlineLibraryEntryId.spell === id} unrecommended={isUnfamiliar} + disabled={!isAvailable} > @@ -79,9 +82,7 @@ const InactiveSpellsListItem: FC = props => { diff --git a/src/main_window/routes/characters/character/spells/InactiveZibiljaRitualsListItem.tsx b/src/main_window/routes/characters/character/spells/InactiveZibiljaRitualsListItem.tsx index 5b487b76..891a3ecc 100644 --- a/src/main_window/routes/characters/character/spells/InactiveZibiljaRitualsListItem.tsx +++ b/src/main_window/routes/characters/character/spells/InactiveZibiljaRitualsListItem.tsx @@ -3,20 +3,27 @@ import { fromRaw } from "../../../../../shared/domain/adventurePoints/improvemen import { DisplayedInactiveZibiljaRitual } from "../../../../../shared/domain/rated/spellInactive.ts" import { SpellsSortOrder } from "../../../../../shared/domain/sortOrders.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useInactiveActivatableActions } from "../../../../hooks/ratedActions.ts" +import { addZibiljaRitual } from "../../../../slices/magicalActions/zibiljaRitualsSlice.ts" import { InactiveMagicalActionsListItem } from "./InactiveMagicalActionsListItem.tsx" type Props = { insertTopMargin?: boolean zibiljaRitual: DisplayedInactiveZibiljaRitual sortOrder: SpellsSortOrder - add: (id: number) => void } const InactiveZibiljaRitualsListItem: FC = props => { - const { insertTopMargin, zibiljaRitual, sortOrder, add } = props + const { insertTopMargin, zibiljaRitual, sortOrder } = props const translate = useTranslate() + const { handleAdd } = useInactiveActivatableActions( + zibiljaRitual.static.id, + fromRaw(zibiljaRitual.static.improvement_cost), + addZibiljaRitual, + ) + return ( = props => { groupName={translate("Zibilja Rituals")} checkPenalty={zibiljaRitual.static.check_penalty} improvementCost={fromRaw(zibiljaRitual.static.improvement_cost)} - add={add} + add={handleAdd} /> ) } diff --git a/src/main_window/routes/characters/character/spells/Spells.tsx b/src/main_window/routes/characters/character/spells/Spells.tsx index f116ab86..a49e097e 100644 --- a/src/main_window/routes/characters/character/spells/Spells.tsx +++ b/src/main_window/routes/characters/character/spells/Spells.tsx @@ -30,73 +30,7 @@ import { selectVisibleInactiveSpellworks, } from "../../../../selectors/spellSelectors.ts" import { addCantrip, removeCantrip } from "../../../../slices/cantripsSlice.ts" -import { - addAnimistPower, - decrementAnimistPower, - incrementAnimistPower, - removeAnimistPower, -} from "../../../../slices/magicalActions/animistPowersSlice.ts" -import { - addCurse, - decrementCurse, - incrementCurse, - removeCurse, -} from "../../../../slices/magicalActions/cursesSlice.ts" -import { - addDominationRitual, - decrementDominationRitual, - incrementDominationRitual, - removeDominationRitual, -} from "../../../../slices/magicalActions/dominationRitualsSlice.ts" -import { - addElvenMagicalSong, - decrementElvenMagicalSong, - incrementElvenMagicalSong, - removeElvenMagicalSong, -} from "../../../../slices/magicalActions/elvenMagicalSongsSlice.ts" -import { - addGeodeRitual, - decrementGeodeRitual, - incrementGeodeRitual, - removeGeodeRitual, -} from "../../../../slices/magicalActions/geodeRitualsSlice.ts" -import { - addJesterTrick, - decrementJesterTrick, - incrementJesterTrick, - removeJesterTrick, -} from "../../../../slices/magicalActions/jesterTricksSlice.ts" -import { - addMagicalDance, - decrementMagicalDance, - incrementMagicalDance, - removeMagicalDance, -} from "../../../../slices/magicalActions/magicalDancesSlice.ts" -import { - addMagicalMelody, - decrementMagicalMelody, - incrementMagicalMelody, - removeMagicalMelody, -} from "../../../../slices/magicalActions/magicalMelodiesSlice.ts" -import { - addZibiljaRitual, - decrementZibiljaRitual, - incrementZibiljaRitual, - removeZibiljaRitual, -} from "../../../../slices/magicalActions/zibiljaRitualsSlice.ts" -import { - addRitual, - decrementRitual, - incrementRitual, - removeRitual, -} from "../../../../slices/ritualsSlice.ts" import { changeSpellsSortOrder, selectSpellsSortOrder } from "../../../../slices/settingsSlice.ts" -import { - addSpell, - decrementSpell, - incrementSpell, - removeSpell, -} from "../../../../slices/spellsSlice.ts" import { ActiveAnimistPowersListItem } from "./ActiveAnimistPowersListItem.tsx" import { ActiveCantripsListItem } from "./ActiveCantripsListItem.tsx" import { ActiveCursesListItem } from "./ActiveCursesListItem.tsx" @@ -191,163 +125,10 @@ export const Spells: FC = () => { ], ) - const handleAddCantrip = useCallback((id: number) => dispatch(addCantrip(id)), [dispatch]) - const handleRemoveCantrip = useCallback((id: number) => dispatch(removeCantrip(id)), [dispatch]) - - const handleAddSpell = useCallback((id: number) => dispatch(addSpell(id)), [dispatch]) - const handleRemoveSpell = useCallback((id: number) => dispatch(removeSpell(id)), [dispatch]) - const handleAddSpellPoint = useCallback((id: number) => dispatch(incrementSpell(id)), [dispatch]) - const handleRemoveSpellPoint = useCallback( - (id: number) => dispatch(decrementSpell(id)), - [dispatch], - ) - - const handleAddRitual = useCallback((id: number) => dispatch(addRitual(id)), [dispatch]) - const handleRemoveRitual = useCallback((id: number) => dispatch(removeRitual(id)), [dispatch]) - const handleAddRitualPoint = useCallback( - (id: number) => dispatch(incrementRitual(id)), - [dispatch], - ) - const handleRemoveRitualPoint = useCallback( - (id: number) => dispatch(decrementRitual(id)), - [dispatch], - ) - - const handleAddCurse = useCallback((id: number) => dispatch(addCurse(id)), [dispatch]) - const handleRemoveCurse = useCallback((id: number) => dispatch(removeCurse(id)), [dispatch]) - const handleAddCursePoint = useCallback((id: number) => dispatch(incrementCurse(id)), [dispatch]) - const handleRemoveCursePoint = useCallback( - (id: number) => dispatch(decrementCurse(id)), - [dispatch], - ) - - const handleAddElvenMagicalSong = useCallback( - (id: number) => dispatch(addElvenMagicalSong(id)), - [dispatch], - ) - const handleRemoveElvenMagicalSong = useCallback( - (id: number) => dispatch(removeElvenMagicalSong(id)), - [dispatch], - ) - const handleAddElvenMagicalSongPoint = useCallback( - (id: number) => dispatch(incrementElvenMagicalSong(id)), - [dispatch], - ) - const handleRemoveElvenMagicalSongPoint = useCallback( - (id: number) => dispatch(decrementElvenMagicalSong(id)), - [dispatch], - ) - - const handleAddDominationRitual = useCallback( - (id: number) => dispatch(addDominationRitual(id)), - [dispatch], - ) - const handleRemoveDominationRitual = useCallback( - (id: number) => dispatch(removeDominationRitual(id)), - [dispatch], - ) - const handleAddDominationRitualPoint = useCallback( - (id: number) => dispatch(incrementDominationRitual(id)), - [dispatch], - ) - const handleRemoveDominationRitualPoint = useCallback( - (id: number) => dispatch(decrementDominationRitual(id)), - [dispatch], - ) - - const handleAddMagicalDance = useCallback( - (id: number) => dispatch(addMagicalDance(id)), - [dispatch], - ) - const handleRemoveMagicalDance = useCallback( - (id: number) => dispatch(removeMagicalDance(id)), - [dispatch], - ) - const handleAddMagicalDancePoint = useCallback( - (id: number) => dispatch(incrementMagicalDance(id)), - [dispatch], - ) - const handleRemoveMagicalDancePoint = useCallback( - (id: number) => dispatch(decrementMagicalDance(id)), - [dispatch], - ) - - const handleAddMagicalMelody = useCallback( - (id: number) => dispatch(addMagicalMelody(id)), - [dispatch], - ) - const handleRemoveMagicalMelody = useCallback( - (id: number) => dispatch(removeMagicalMelody(id)), - [dispatch], - ) - const handleAddMagicalMelodyPoint = useCallback( - (id: number) => dispatch(incrementMagicalMelody(id)), - [dispatch], - ) - const handleRemoveMagicalMelodyPoint = useCallback( - (id: number) => dispatch(decrementMagicalMelody(id)), - [dispatch], - ) - - const handleAddJesterTrick = useCallback((id: number) => dispatch(addJesterTrick(id)), [dispatch]) - const handleRemoveJesterTrick = useCallback( - (id: number) => dispatch(removeJesterTrick(id)), - [dispatch], - ) - const handleAddJesterTrickPoint = useCallback( - (id: number) => dispatch(incrementJesterTrick(id)), - [dispatch], - ) - const handleRemoveJesterTrickPoint = useCallback( - (id: number) => dispatch(decrementJesterTrick(id)), - [dispatch], - ) - - const handleAddAnimistPower = useCallback( - (id: number) => dispatch(addAnimistPower(id)), - [dispatch], - ) - const handleRemoveAnimistPower = useCallback( - (id: number) => dispatch(removeAnimistPower(id)), - [dispatch], - ) - const handleAddAnimistPowerPoint = useCallback( - (id: number) => dispatch(incrementAnimistPower(id)), - [dispatch], - ) - const handleRemoveAnimistPowerPoint = useCallback( - (id: number) => dispatch(decrementAnimistPower(id)), - [dispatch], - ) - - const handleAddGeodeRitual = useCallback((id: number) => dispatch(addGeodeRitual(id)), [dispatch]) - const handleRemoveGeodeRitual = useCallback( - (id: number) => dispatch(removeGeodeRitual(id)), - [dispatch], - ) - const handleAddGeodeRitualPoint = useCallback( - (id: number) => dispatch(incrementGeodeRitual(id)), - [dispatch], - ) - const handleRemoveGeodeRitualPoint = useCallback( - (id: number) => dispatch(decrementGeodeRitual(id)), - [dispatch], - ) - - const handleAddZibiljaRitual = useCallback( - (id: number) => dispatch(addZibiljaRitual(id)), - [dispatch], - ) - const handleRemoveZibiljaRitual = useCallback( - (id: number) => dispatch(removeZibiljaRitual(id)), - [dispatch], - ) - const handleAddZibiljaRitualPoint = useCallback( - (id: number) => dispatch(incrementZibiljaRitual(id)), - [dispatch], - ) - const handleRemoveZibiljaRitualPoint = useCallback( - (id: number) => dispatch(decrementZibiljaRitual(id)), + // TODO: Check available AP + const handleAddCantrip = useCallback((id: number) => dispatch(addCantrip({ id })), [dispatch]) + const handleRemoveCantrip = useCallback( + (id: number) => dispatch(removeCantrip({ id })), [dispatch], ) @@ -391,17 +172,17 @@ export const Spells: FC = () => {
- {translate("spells.header.name")} + {translate("Name")} - {translate("spells.header.property")} - {sortOrder === "group" ? ` / ${translate("spells.header.group")}` : null} + {translate("Property")} + {sortOrder === "group" ? ` / ${translate("Group")}` : null} - {translate("spells.header.check")} - - {translate("spells.header.checkmodifier")} + {translate("Check")} + + {translate("Mod")} - - {translate("spells.header.improvementcost")} + + {translate("IC")} @@ -429,7 +210,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} spell={x} sortOrder={sortOrder} - add={handleAddSpell} /> ) @@ -440,7 +220,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} ritual={x} sortOrder={sortOrder} - add={handleAddRitual} /> ) @@ -451,7 +230,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} curse={x} sortOrder={sortOrder} - add={handleAddCurse} /> ) @@ -462,7 +240,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} elvenMagicalSong={x} sortOrder={sortOrder} - add={handleAddElvenMagicalSong} /> ) @@ -473,7 +250,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} dominationRitual={x} sortOrder={sortOrder} - add={handleAddDominationRitual} /> ) @@ -484,7 +260,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} magicalDance={x} sortOrder={sortOrder} - add={handleAddMagicalDance} /> ) @@ -495,7 +270,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} magicalMelody={x} sortOrder={sortOrder} - add={handleAddMagicalMelody} /> ) @@ -506,7 +280,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} jesterTrick={x} sortOrder={sortOrder} - add={handleAddJesterTrick} /> ) @@ -517,7 +290,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} animistPower={x} sortOrder={sortOrder} - add={handleAddAnimistPower} /> ) @@ -528,7 +300,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} geodeRitual={x} sortOrder={sortOrder} - add={handleAddGeodeRitual} /> ) @@ -539,7 +310,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, inactiveList[i - 1])} zibiljaRitual={x} sortOrder={sortOrder} - add={handleAddZibiljaRitual} /> ) @@ -589,20 +359,20 @@ export const Spells: FC = () => {
- {translate("spells.header.name")} + {translate("Name")} - {translate("spells.header.property")} - {sortOrder === "group" ? ` / ${translate("spells.header.group")}` : null} + {translate("Property")} + {sortOrder === "group" ? ` / ${translate("Group")}` : null} - - {translate("spells.header.skillrating")} + + {translate("SR")} - {translate("spells.header.check")} - - {translate("spells.header.checkmodifier")} + {translate("Check")} + + {translate("Mod")} - - {translate("spells.header.improvementcost")} + + {translate("IC")} {canRemove ? : null} @@ -631,9 +401,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} spell={x} sortOrder={sortOrder} - addPoint={handleAddSpellPoint} - removePoint={handleRemoveSpellPoint} - remove={handleRemoveSpell} /> ) @@ -644,9 +411,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} ritual={x} sortOrder={sortOrder} - addPoint={handleAddRitualPoint} - removePoint={handleRemoveRitualPoint} - remove={handleRemoveRitual} /> ) @@ -657,9 +421,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} curse={x} sortOrder={sortOrder} - addPoint={handleAddCursePoint} - removePoint={handleRemoveCursePoint} - remove={handleRemoveCurse} /> ) @@ -670,9 +431,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} elvenMagicalSong={x} sortOrder={sortOrder} - addPoint={handleAddElvenMagicalSongPoint} - removePoint={handleRemoveElvenMagicalSongPoint} - remove={handleRemoveElvenMagicalSong} /> ) @@ -683,9 +441,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} dominationRitual={x} sortOrder={sortOrder} - addPoint={handleAddDominationRitualPoint} - removePoint={handleRemoveDominationRitualPoint} - remove={handleRemoveDominationRitual} /> ) @@ -696,9 +451,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} magicalDance={x} sortOrder={sortOrder} - addPoint={handleAddMagicalDancePoint} - removePoint={handleRemoveMagicalDancePoint} - remove={handleRemoveMagicalDance} /> ) @@ -709,9 +461,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} magicalMelody={x} sortOrder={sortOrder} - addPoint={handleAddMagicalMelodyPoint} - removePoint={handleRemoveMagicalMelodyPoint} - remove={handleRemoveMagicalMelody} /> ) @@ -722,9 +471,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} jesterTrick={x} sortOrder={sortOrder} - addPoint={handleAddJesterTrickPoint} - removePoint={handleRemoveJesterTrickPoint} - remove={handleRemoveJesterTrick} /> ) @@ -735,9 +481,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} animistPower={x} sortOrder={sortOrder} - addPoint={handleAddAnimistPowerPoint} - removePoint={handleRemoveAnimistPowerPoint} - remove={handleRemoveAnimistPower} /> ) @@ -748,9 +491,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} geodeRitual={x} sortOrder={sortOrder} - addPoint={handleAddGeodeRitualPoint} - removePoint={handleRemoveGeodeRitualPoint} - remove={handleRemoveGeodeRitual} /> ) @@ -761,9 +501,6 @@ export const Spells: FC = () => { insertTopMargin={isTopMarginNeeded(sortOrder, x, activeList[i - 1])} zibiljaRitual={x} sortOrder={sortOrder} - addPoint={handleAddZibiljaRitualPoint} - removePoint={handleRemoveZibiljaRitualPoint} - remove={handleRemoveZibiljaRitual} /> ) diff --git a/src/main_window/selectors/adventurePointSelectors.ts b/src/main_window/selectors/adventurePointSelectors.ts index 24a1b8fd..d6ae1424 100644 --- a/src/main_window/selectors/adventurePointSelectors.ts +++ b/src/main_window/selectors/adventurePointSelectors.ts @@ -12,7 +12,6 @@ import { ImprovementCost, adventurePointsForRange, } from "../../shared/domain/adventurePoints/improvementCost.ts" -import { RatedAdventurePointsCache } from "../../shared/domain/adventurePoints/ratedEntry.ts" import { count } from "../../shared/utils/array.ts" import { selectCurrentCharacter, @@ -43,16 +42,13 @@ import { selectMagicalAndBlessedAdvantagesAndDisadvantagesCache } from "../slice import { SelectAll } from "./basicCapabilitySelectors.ts" const sumRatedMaps = ( - ...ratedMaps: (Record | undefined)[] -): SpentAdventurePoints => + ...ratedMaps: (Record | undefined)[] +): AdventurePointsCache => ratedMaps .flatMap(ratedMap => Object.values(ratedMap ?? {})) .reduce( - (acc, rated) => ({ - general: acc.general + rated.cachedAdventurePoints.general, - bound: acc.bound + rated.cachedAdventurePoints.bound, - }), - { general: 0, bound: 0 }, + (acc, rated) => addAdventurePointsCaches(acc, rated.cachedAdventurePoints), + emptyAdventurePointsCache, ) /** @@ -104,7 +100,7 @@ export const selectAdventurePointsSpentOnLiturgicalChants = createSelector( sumRatedMaps, ) -const sumTinyActivatables = (tinyActivatables: TinyActivatable[]): SpentAdventurePoints => ({ +const sumTinyActivatables = (tinyActivatables: TinyActivatable[]): AdventurePointsCache => ({ general: count(tinyActivatables, isTinyActivatableActive), bound: 0, }) diff --git a/src/main_window/selectors/attributeSelectors.ts b/src/main_window/selectors/attributeSelectors.ts index b27321a4..82434b63 100644 --- a/src/main_window/selectors/attributeSelectors.ts +++ b/src/main_window/selectors/attributeSelectors.ts @@ -1,10 +1,15 @@ import { createSelector } from "@reduxjs/toolkit" import { Attribute } from "optolith-database-schema/types/Attribute" +import { getCarryingCapacity } from "../../shared/domain/equipment.ts" import { AdvantageIdentifier, AttributeIdentifier, OptionalRuleIdentifier, } from "../../shared/domain/identifier.ts" +import { + createEmptyDynamicAttribute, + getAttributeValue, +} from "../../shared/domain/rated/attribute.ts" import { getAttributeMaximum, getAttributeMinimaByAssociatedAttributes, @@ -15,7 +20,6 @@ import { import { Rated } from "../../shared/domain/rated/ratedEntry.ts" import { isNotNullish } from "../../shared/utils/nullable.ts" import { createPropertySelector } from "../../shared/utils/redux.ts" -import { attributeValue, createInitialDynamicAttribute } from "../slices/attributesSlice.ts" import { selectAttributeAdjustmentId, selectBlessedPrimaryAttributeDependencies, @@ -160,7 +164,7 @@ export const selectVisibleAttributes = createSelector( .sort((a, b) => a.id - b.id) .map(attribute => { const dynamicAttribute = - getDynamicAttributeById(attribute.id) ?? createInitialDynamicAttribute(attribute.id) + getDynamicAttributeById(attribute.id) ?? createEmptyDynamicAttribute(attribute.id) const minimum = getAttributeMinimum( derivedCharacteristics.lifePoints.purchased, @@ -228,7 +232,7 @@ export const selectVisibleAttributes = createSelector( */ export const selectCarryingCapacity = createSelector( createPropertySelector(selectDynamicAttributes, AttributeIdentifier.Strength), - (strength): number => (strength?.value ?? 8) * 2, + (strength): number => getCarryingCapacity(getAttributeValue(strength)), ) /** @@ -258,7 +262,7 @@ export const selectAvailableAdjustments = createSelector( const canNotSwitch = current !== undefined && current.maximum !== undefined && - current.maximum - selectableAdjustment.value < attributeValue(current.dynamic) + current.maximum - selectableAdjustment.value < getAttributeValue(current.dynamic) if (canNotSwitch) { return { diff --git a/src/main_window/selectors/combatTechniquesSelectors.ts b/src/main_window/selectors/combatTechniquesSelectors.ts index 862affdc..a4d2c4fd 100644 --- a/src/main_window/selectors/combatTechniquesSelectors.ts +++ b/src/main_window/selectors/combatTechniquesSelectors.ts @@ -9,6 +9,7 @@ import { } from "../../shared/domain/identifier.ts" import { getHighestAttributeValue } from "../../shared/domain/rated/attribute.ts" import { + createEmptyDynamicCombatTechnique, getAttackBaseForClose, getAttackBaseForRanged, getParryBaseForClose, @@ -26,8 +27,6 @@ import { selectDynamicAdvantages, selectDynamicGeneralSpecialAbilities, } from "../slices/characterSlice.ts" -import { createInitialDynamicCloseCombatTechnique } from "../slices/closeCombatTechniqueSlice.ts" -import { createInitialDynamicRangedCombatTechnique } from "../slices/rangedCombatTechniqueSlice.ts" import { SelectAll, SelectGetById } from "./basicCapabilitySelectors.ts" import { selectCanRemove, selectIsInCharacterCreation } from "./characterSelectors.ts" import { selectFilterApplyingRatedDependencies } from "./dependencySelectors.ts" @@ -123,7 +122,7 @@ export const selectVisibleCloseCombatTechniques = createSelector( .map(combatTechnique => { const dynamicCloseCombatTechnique = getDynamicCloseCombatTechniqueById(combatTechnique.id) ?? - createInitialDynamicCloseCombatTechnique(combatTechnique.id) + createEmptyDynamicCombatTechnique(combatTechnique.id) const minimum = getCombatTechniqueMinimum( rangedCombatTechniquesAt10, @@ -220,7 +219,7 @@ export const selectVisibleRangedCombatTechniques = createSelector( const dynamicRangedCombatTechnique = getDynamicRangedCombatTechniqueById(combatTechnique.id) ?? - createInitialDynamicRangedCombatTechnique(combatTechnique.id) + createEmptyDynamicCombatTechnique(combatTechnique.id) const minimum = getCombatTechniqueMinimum( rangedCombatTechniquesAt10, diff --git a/src/main_window/selectors/derivedCharacteristicsSelectors.ts b/src/main_window/selectors/derivedCharacteristicsSelectors.ts index 0d16ff8a..957e419e 100644 --- a/src/main_window/selectors/derivedCharacteristicsSelectors.ts +++ b/src/main_window/selectors/derivedCharacteristicsSelectors.ts @@ -3,7 +3,11 @@ import { DerivedCharacteristic, DerivedCharacteristicTranslation, } from "optolith-database-schema/types/DerivedCharacteristic" -import { firstLevel, isActive } from "../../shared/domain/activatable/activatableEntry.ts" +import { + firstLevel, + isActive, + isOptionActive, +} from "../../shared/domain/activatable/activatableEntry.ts" import { modifierByIsActive, modifierByIsActives, @@ -20,10 +24,10 @@ import { MagicalSpecialAbilityIdentifier, OptionalRuleIdentifier, } from "../../shared/domain/identifier.ts" +import { getAttributeValue } from "../../shared/domain/rated/attribute.ts" import { Rated } from "../../shared/domain/rated/ratedEntry.ts" import { filterNonNullable } from "../../shared/utils/array.ts" import { createPropertySelector } from "../../shared/utils/redux.ts" -import { attributeValue } from "../slices/attributesSlice.ts" import { selectArcaneEnergyPermanentlyLost, selectArcaneEnergyPermanentlyLostBoughtBack, @@ -119,7 +123,7 @@ export const selectLifePoints = createSelector( if (race === undefined || staticEntry === undefined) { return undefined } else { - const conValue = attributeValue(con) + const conValue = getAttributeValue(con) const base = race.base_values.life_points + conValue * 2 const modifier = modifierByLevel(incrementor, decrementor) const value = base + modifier + purchased - permanentlyLost @@ -289,7 +293,7 @@ export const selectKarmaPoints = createSelector( ) const divideAttributeSumByRound = (attributes: (Rated | undefined)[], divisor: number) => - Math.round(attributes.reduce((acc, attr) => acc + attributeValue(attr), 0) / divisor) + Math.round(attributes.reduce((acc, attr) => acc + getAttributeValue(attr), 0) / divisor) /** * Returns the static and dynamic values for spirit. @@ -453,13 +457,7 @@ export const selectMovement = createSelector( return undefined } else { const oneLegged = 3 - const isOneLeggedActive = - maimed?.instances.some( - instance => - instance.options?.[0]?.type === "Predefined" && - instance.options?.[0]?.id.type === "Generic" && - instance.options?.[0]?.id.value === oneLegged, - ) ?? false + const isOneLeggedActive = isOptionActive(maimed, { tag: "General", general: oneLegged }) const base = isOneLeggedActive ? Math.round(race.base_values.movement / 2) diff --git a/src/main_window/selectors/liturgicalChantSelectors.ts b/src/main_window/selectors/liturgicalChantSelectors.ts index 9a607a1a..a2c870f0 100644 --- a/src/main_window/selectors/liturgicalChantSelectors.ts +++ b/src/main_window/selectors/liturgicalChantSelectors.ts @@ -1,5 +1,7 @@ import { createSelector } from "@reduxjs/toolkit" +import { Blessing } from "optolith-database-schema/types/Blessing" import { + TinyActivatable, getOptions, isTinyActivatableActive, } from "../../shared/domain/activatable/activatableEntry.ts" @@ -72,23 +74,30 @@ const selectActiveAspectKnowledges = createSelector( ), aspectKnowledge => getOptions(aspectKnowledge).flatMap(option => - option.type === "Predefined" && option.id.type === "Aspect" ? [option.id.value] : [], + option.type === "Predefined" && option.id.tag === "Aspect" ? [option.id.aspect] : [], ), ) /** * Returns the blessings, split by active and inactive. */ -export const selectVisibleBlessings = createSelector( +const selectVisibleBlessings = createSelector( SelectAll.Static.Blessings, SelectGetById.Dynamic.Blessing, - ( - staticBlessings, - getDynamicBlessingById, - ): [active: DisplayedActiveBlessing[], inactive: DisplayedActiveBlessing[]] => + (staticBlessings, getDynamicBlessingById) => partition( - staticBlessings.map(blessing => ({ kind: "blessing", static: blessing })), - staticBlessing => isTinyActivatableActive(getDynamicBlessingById(staticBlessing.static.id)), + staticBlessings.map(staticBlessing => ({ + kind: "blessing" as const, + static: staticBlessing, + dynamic: getDynamicBlessingById(staticBlessing.id), + })), + ( + blessing, + ): blessing is { + kind: "blessing" + static: Blessing + dynamic: TinyActivatable + } => isTinyActivatableActive(blessing.dynamic), ), ) diff --git a/src/main_window/selectors/profileSelectors.ts b/src/main_window/selectors/profileSelectors.ts index 65e96dd0..626e3729 100644 --- a/src/main_window/selectors/profileSelectors.ts +++ b/src/main_window/selectors/profileSelectors.ts @@ -1,5 +1,6 @@ import { createSelector } from "@reduxjs/toolkit" import { ProfessionIdentifier } from "../../shared/domain/identifier.ts" +import { isInRange } from "../../shared/utils/range.ts" import { selectIsCharacterCreationFinished } from "../slices/characterSlice.ts" import { selectAdventurePointsAvailable } from "./adventurePointSelectors.ts" import { selectCurrentProfession } from "./professionSelectors.ts" @@ -17,8 +18,7 @@ export const selectShowFinishCharacterCreation = createSelector( */ export const selectCanFinishCharacterCreation = createSelector( selectAdventurePointsAvailable, - (availableAdventurePoints): boolean => - availableAdventurePoints >= 0 && availableAdventurePoints <= 10, + (availableAdventurePoints): boolean => isInRange([0, 10], availableAdventurePoints), ) /** diff --git a/src/main_window/selectors/skillsSelectors.ts b/src/main_window/selectors/skillsSelectors.ts index 87e6dc06..16cc7e83 100644 --- a/src/main_window/selectors/skillsSelectors.ts +++ b/src/main_window/selectors/skillsSelectors.ts @@ -6,7 +6,7 @@ import { } from "../../shared/domain/identifier.ts" import { getHighestAttributeValue } from "../../shared/domain/rated/attribute.ts" import { Rated } from "../../shared/domain/rated/ratedEntry.ts" -import { getSkillCommonness } from "../../shared/domain/rated/skill.ts" +import { createEmptyDynamicSkill, getSkillCommonness } from "../../shared/domain/rated/skill.ts" import { getSkillMaximum, getSkillMinimum, @@ -18,7 +18,6 @@ import { selectDynamicAdvantages, selectDynamicGeneralSpecialAbilities, } from "../slices/characterSlice.ts" -import { createInitialDynamicSkill } from "../slices/skillsSlice.ts" import { SelectAll, SelectGetById } from "./basicCapabilitySelectors.ts" import { selectCanRemove, selectIsInCharacterCreation } from "./characterSelectors.ts" import { selectCurrentCulture } from "./cultureSelectors.ts" @@ -72,7 +71,7 @@ export const selectVisibleSkills = createSelector( filterApplyingDependencies, ): DisplayedSkill[] => staticSkills.map(skill => { - const dynamicSkill = getDynamicSkillById(skill.id) ?? createInitialDynamicSkill(skill.id) + const dynamicSkill = getDynamicSkillById(skill.id) ?? createEmptyDynamicSkill(skill.id) const minimum = getSkillMinimum( getDynamicSkillById, diff --git a/src/main_window/selectors/spellSelectors.ts b/src/main_window/selectors/spellSelectors.ts index 80ebab16..88ab22cd 100644 --- a/src/main_window/selectors/spellSelectors.ts +++ b/src/main_window/selectors/spellSelectors.ts @@ -3,8 +3,10 @@ import { getOptions } from "../../shared/domain/activatable/activatableEntry.ts" import { AdvantageIdentifier, MagicalSpecialAbilityIdentifier, + MagicalTraditionIdentifier, createIdentifierObject, } from "../../shared/domain/identifier.ts" +import { getImprovementCostForAnimistPower } from "../../shared/domain/rated/animistPower.ts" import { countActiveSpellworks, countActiveSpellworksByImprovementCost, @@ -78,6 +80,7 @@ const selectSpellworksAbove10ByProperty = createSelector( SelectGetById.Static.AnimistPower, SelectGetById.Static.GeodeRitual, SelectGetById.Static.ZibiljaRitual, + SelectGetById.Static.MagicalRune, SelectAll.Dynamic.Spells, SelectAll.Dynamic.Rituals, SelectAll.Dynamic.Curses, @@ -89,6 +92,7 @@ const selectSpellworksAbove10ByProperty = createSelector( SelectAll.Dynamic.AnimistPowers, SelectAll.Dynamic.GeodeRituals, SelectAll.Dynamic.ZibiljaRituals, + SelectAll.Dynamic.MagicalRunes, ( getStaticSpellById, getStaticRitualById, @@ -101,6 +105,7 @@ const selectSpellworksAbove10ByProperty = createSelector( getStaticAnimistPowerById, getStaticGeodeRitualById, getStaticZibiljaRitualById, + getStaticMagicalRuneById, dynamicSpells, dynamicRituals, dynamicCurses, @@ -112,6 +117,7 @@ const selectSpellworksAbove10ByProperty = createSelector( dynamicAnimistPowers, dynamicGeodeRituals, dynamicZibiljaRituals, + dynamicMagicalRunes, ) => getSpellworksAbove10ByProperty( id => @@ -139,6 +145,8 @@ const selectSpellworksAbove10ByProperty = createSelector( return getStaticGeodeRitualById(id.geode_ritual) case "ZibiljaRitual": return getStaticZibiljaRitualById(id.zibilja_ritual) + case "MagicalRune": + return getStaticMagicalRuneById(id.magical_rune) default: return assertExhaustive(id) } @@ -154,6 +162,7 @@ const selectSpellworksAbove10ByProperty = createSelector( dynamicAnimistPowers, dynamicGeodeRituals, dynamicZibiljaRituals, + dynamicMagicalRunes, ), ) @@ -164,7 +173,7 @@ const selectActivePropertyKnowledges = createSelector( ), propertyKnowledge => getOptions(propertyKnowledge).flatMap(option => - option.type === "Predefined" && option.id.type === "Property" ? [option.id.value] : [], + option.type === "Predefined" && option.id.tag === "Property" ? [option.id.property] : [], ), ) @@ -646,6 +655,8 @@ export const selectVisibleActiveAnimistPowers = createSelector( selectFilterApplyingRatedDependencies, selectSpellworksAbove10ByProperty, selectActivePropertyKnowledges, + SelectGetById.Static.Patron, + SelectGetById.Dynamic.MagicalTradition, ( getStaticAnimistPowerById, dynamicAnimistPowers, @@ -656,8 +667,12 @@ export const selectVisibleActiveAnimistPowers = createSelector( filterApplyingDependencies, spellworksAbove10ByProperty, activePropertyKnowledges, + getStaticPatronById, + getDynamicMagicalTraditionById, ): DisplayedActiveAnimistPower[] => { - if (startExperienceLevel === undefined) { + const traditionAnimists = getDynamicMagicalTraditionById(MagicalTraditionIdentifier.Animisten) + + if (startExperienceLevel === undefined || traditionAnimists === undefined) { return [] } @@ -673,6 +688,18 @@ export const selectVisibleActiveAnimistPowers = createSelector( spellworksAbove10ByProperty, activePropertyKnowledges, ) + .map(animistPower => ({ + ...animistPower, + improvementCost: getImprovementCostForAnimistPower( + getStaticPatronById, + traditionAnimists, + animistPower.static.improvement_cost, + ), + })) + .filter( + (animistPower): animistPower is DisplayedActiveAnimistPower => + animistPower.improvementCost !== undefined, + ) }, ) @@ -1013,7 +1040,34 @@ export const selectVisibleInactiveAnimistPowers = createSelector( selectActiveMagicalTraditions, selectIsMaximumOfSpellworksReached, selectIsEntryAvailable, - getInactiveAnimistPowers, + SelectGetById.Static.Patron, + SelectGetById.Dynamic.MagicalTradition, + + ( + staticAnimistPowers, + getDynamicAnimistPowerById, + activeMagicalTraditions, + isMaximumCountReached, + isEntryAvailable, + getStaticPatronById, + getDynamicMagicalTraditionById, + ) => { + const traditionAnimists = getDynamicMagicalTraditionById(MagicalTraditionIdentifier.Animisten) + + if (traditionAnimists === undefined) { + return [] + } + + return getInactiveAnimistPowers( + staticAnimistPowers, + getDynamicAnimistPowerById, + activeMagicalTraditions, + isMaximumCountReached, + isEntryAvailable, + getStaticPatronById, + traditionAnimists, + ) + }, ) /** diff --git a/src/main_window/slices/activatableRatedSlice.ts b/src/main_window/slices/activatableRatedSlice.ts index 17c17c6f..7a85ec82 100644 --- a/src/main_window/slices/activatableRatedSlice.ts +++ b/src/main_window/slices/activatableRatedSlice.ts @@ -1,15 +1,7 @@ import { ActionCreatorWithPayload, AnyAction, Draft, createAction } from "@reduxjs/toolkit" import { ImprovementCost } from "../../shared/domain/adventurePoints/improvementCost.ts" -import { - BoundAdventurePoints, - cachedAdventurePointsForActivatable, -} from "../../shared/domain/adventurePoints/ratedEntry.ts" -import { RatedDependency } from "../../shared/domain/rated/ratedDependency.ts" -import { - ActivatableRated, - ActivatableRatedMap, - ActivatableRatedValue, -} from "../../shared/domain/rated/ratedEntry.ts" +import { cachedAdventurePointsForActivatable } from "../../shared/domain/adventurePoints/ratedEntry.ts" +import { ActivatableRated, ActivatableRatedMap } from "../../shared/domain/rated/ratedEntry.ts" import { Reducer, createImmerReducer } from "../../shared/utils/redux.ts" import { CharacterState } from "./characterSlice.ts" import { DatabaseState } from "./databaseSlice.ts" @@ -19,37 +11,6 @@ import { DatabaseState } from "./databaseSlice.ts" * enhancements. */ export type ActivatableRatedSlice = { - /** - * Creates a new entry with an initial value if active. The initial adventure - * points cache is calculated from the initial value. - */ - create: ( - database: DatabaseState, - id: number, - value: ActivatableRatedValue, - options?: Partial<{ - dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] - }>, - ) => ActivatableRated - - /** - * Creates a new entry with no initial value. - */ - createInitial: ( - id: number, - options?: Partial<{ - dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] - }>, - ) => ActivatableRated - - /** - * Takes an entry that may not exist (because its instance has not been used - * yet) and returns its value. - */ - getValue: (entry: ActivatableRated | undefined) => ActivatableRatedValue - /** * The actions that can be dispatched to modify the state. */ @@ -57,22 +18,27 @@ export type ActivatableRatedSlice = { /** * Add the entry with the given id. */ - addAction: ActionCreatorWithPayload + addAction: ActionCreatorWithPayload<{ id: number }, `${N}/add${E}`> /** * Remove the entry with the given id. */ - removeAction: ActionCreatorWithPayload + removeAction: ActionCreatorWithPayload<{ id: number }, `${N}/remove${E}`> /** * Increments the value of the entry with the given id. */ - incrementAction: ActionCreatorWithPayload + incrementAction: ActionCreatorWithPayload<{ id: number }, `${N}/increment${E}`> /** * Decrements the value of the entry with the given id. */ - decrementAction: ActionCreatorWithPayload + decrementAction: ActionCreatorWithPayload<{ id: number }, `${N}/decrement${E}`> + + /** + * Sets the value of the entry with the given id. + */ + setAction: ActionCreatorWithPayload<{ id: number; value: number }, `${N}/set${E}`> } /** @@ -88,103 +54,87 @@ export const createActivatableRatedSlice = ( namespace: N entityName: E getState: (state: Draft) => Draft - getImprovementCost: (id: number, database: DatabaseState) => ImprovementCost + getImprovementCost: ( + id: number, + database: DatabaseState, + character: CharacterState, + ) => ImprovementCost + createEmptyActivatableRated: (id: number) => ActivatableRated }): ActivatableRatedSlice => { - const updateCachedAdventurePoints = (entry: Draft, database: DatabaseState) => { + const updateCachedAdventurePoints = ( + entry: Draft, + database: DatabaseState, + character: Draft, + ) => { entry.cachedAdventurePoints = cachedAdventurePointsForActivatable( entry.value, entry.boundAdventurePoints, - config.getImprovementCost(entry.id, database), + config.getImprovementCost(entry.id, database, character), ) return entry } - const create: ActivatableRatedSlice["create"] = ( - database, - id, - value, - { dependencies = [], boundAdventurePoints = [] } = {}, - ) => - updateCachedAdventurePoints( - { - id, - value: value === undefined ? undefined : Math.max(0, value), - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - }, - database, - ) - - const createInitial: ActivatableRatedSlice["createInitial"] = ( - id, - { dependencies = [], boundAdventurePoints = [] } = {}, - ) => ({ - id, - value: undefined, - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - enhancements: {}, - }) - - const getValue: ActivatableRatedSlice["getValue"] = entry => entry?.value - - const addAction = createAction( + const addAction = createAction<{ id: number }, `${N}/add${E}`>( `${config.namespace}/add${config.entityName}`, ) - const removeAction = createAction( + const removeAction = createAction<{ id: number }, `${N}/remove${E}`>( `${config.namespace}/remove${config.entityName}`, ) - const incrementAction = createAction( + const incrementAction = createAction<{ id: number }, `${N}/increment${E}`>( `${config.namespace}/increment${config.entityName}`, ) - const decrementAction = createAction( + const decrementAction = createAction<{ id: number }, `${N}/decrement${E}`>( `${config.namespace}/decrement${config.entityName}`, ) + const setAction = createAction<{ id: number; value: number }, `${N}/set${E}`>( + `${config.namespace}/set${config.entityName}`, + ) + const reducer = createImmerReducer( (state: Draft, action, database: DatabaseState) => { if (addAction.match(action)) { - config.getState(state)[action.payload] ??= create(database, action.payload, 0) + config.getState(state)[action.payload.id] ??= config.createEmptyActivatableRated( + action.payload.id, + ) } else if (removeAction.match(action)) { - delete config.getState(state)[action.payload] + delete config.getState(state)[action.payload.id] } else if (incrementAction.match(action)) { - const entry = (config.getState(state)[action.payload] ??= createInitial(action.payload)) + const entry = (config.getState(state)[action.payload.id] ??= + config.createEmptyActivatableRated(action.payload.id)) if (entry.value === undefined) { entry.value = 0 } else { entry.value++ } - updateCachedAdventurePoints(entry, database) + updateCachedAdventurePoints(entry, database, state) } else if (decrementAction.match(action)) { - const entry = config.getState(state)[action.payload] + const entry = config.getState(state)[action.payload.id] if (entry !== undefined && entry.value !== undefined && entry.value > 0) { entry.value-- - updateCachedAdventurePoints(entry, database) + updateCachedAdventurePoints(entry, database, state) + } + } else if (setAction.match(action)) { + const entry = (config.getState(state)[action.payload.id] ??= + config.createEmptyActivatableRated(action.payload.id)) + if (action.payload.value >= 0) { + entry.value = action.payload.value + updateCachedAdventurePoints(entry, database, state) } } }, ) return { - create, - createInitial, - getValue, actions: { addAction, removeAction, incrementAction, decrementAction, + setAction, }, reducer, } diff --git a/src/main_window/slices/activatableRatedWithEnhancementsSlice.ts b/src/main_window/slices/activatableRatedWithEnhancementsSlice.ts index 99f836db..4a0531ce 100644 --- a/src/main_window/slices/activatableRatedWithEnhancementsSlice.ts +++ b/src/main_window/slices/activatableRatedWithEnhancementsSlice.ts @@ -1,14 +1,8 @@ import { ActionCreatorWithPayload, AnyAction, Draft, createAction } from "@reduxjs/toolkit" import { ImprovementCost } from "../../shared/domain/adventurePoints/improvementCost.ts" -import { - BoundAdventurePoints, - cachedAdventurePointsForActivatableWithEnhancements, -} from "../../shared/domain/adventurePoints/ratedEntry.ts" +import { cachedAdventurePointsForActivatableWithEnhancements } from "../../shared/domain/adventurePoints/ratedEntry.ts" import { RegistrationMethod } from "../../shared/domain/dependencies/registrationHelpers.ts" -import { Enhancement } from "../../shared/domain/rated/enhancement.ts" -import { RatedDependency } from "../../shared/domain/rated/ratedDependency.ts" import { - ActivatableRatedValue, ActivatableRatedWithEnhancements, ActivatableRatedWithEnhancementsMap, } from "../../shared/domain/rated/ratedEntry.ts" @@ -21,40 +15,6 @@ import { DatabaseState } from "./databaseSlice.ts" * enhancements. */ export type ActivatableRatedWithEnhancementsSlice = { - /** - * Creates a new entry with an initial value if active. The initial adventure - * points cache is calculated from the initial value. - */ - create: ( - database: DatabaseState, - id: number, - value: ActivatableRatedValue, - options?: Partial<{ - dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] - enhancements: { - [id: number]: Enhancement - } - }>, - ) => ActivatableRatedWithEnhancements - - /** - * Creates a new entry with no initial value. - */ - createInitial: ( - id: number, - options?: Partial<{ - dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] - }>, - ) => ActivatableRatedWithEnhancements - - /** - * Takes an entry that may not exist (because its instance has not been used - * yet) and returns its value. - */ - getValue: (entry: ActivatableRatedWithEnhancements | undefined) => ActivatableRatedValue - /** * The actions that can be dispatched to modify the state. */ @@ -62,22 +22,27 @@ export type ActivatableRatedWithEnhancementsSlice + addAction: ActionCreatorWithPayload<{ id: number }, `${N}/add${E}`> /** * Remove the entry with the given id. */ - removeAction: ActionCreatorWithPayload + removeAction: ActionCreatorWithPayload<{ id: number }, `${N}/remove${E}`> /** * Increments the value of the entry with the given id. */ - incrementAction: ActionCreatorWithPayload + incrementAction: ActionCreatorWithPayload<{ id: number }, `${N}/increment${E}`> /** * Decrements the value of the entry with the given id. */ - decrementAction: ActionCreatorWithPayload + decrementAction: ActionCreatorWithPayload<{ id: number }, `${N}/decrement${E}`> + + /** + * Sets the value of the entry with the given id. + */ + setAction: ActionCreatorWithPayload<{ id: number; value: number }, `${N}/set${E}`> } /** @@ -112,6 +77,7 @@ export const createActivatableRatedWithEnhancementsSlice = < prerequisites: P[], sourceId: IdO, ) => void + createEmptyActivatableRatedWithEnhancements: (id: number) => ActivatableRatedWithEnhancements }): ActivatableRatedWithEnhancementsSlice => { const updateCachedAdventurePoints = ( entry: Draft, @@ -128,80 +94,48 @@ export const createActivatableRatedWithEnhancementsSlice = < return entry } - const create: ActivatableRatedWithEnhancementsSlice["create"] = ( - database, - id, - value, - { dependencies = [], boundAdventurePoints = [], enhancements = {} } = {}, - ) => - updateCachedAdventurePoints( - { - id, - value: value === undefined ? undefined : Math.max(0, value), - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - enhancements, - }, - database, - ) - - const createInitial: ActivatableRatedWithEnhancementsSlice["createInitial"] = ( - id, - { dependencies = [], boundAdventurePoints = [] } = {}, - ) => ({ - id, - value: undefined, - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - enhancements: {}, - }) - - const getValue: ActivatableRatedWithEnhancementsSlice["getValue"] = entry => entry?.value - - const addAction = createAction( + const addAction = createAction<{ id: number }, `${N}/add${E}`>( `${config.namespace}/add${config.entityName}`, ) - const removeAction = createAction( + const removeAction = createAction<{ id: number }, `${N}/remove${E}`>( `${config.namespace}/remove${config.entityName}`, ) - const incrementAction = createAction( + const incrementAction = createAction<{ id: number }, `${N}/increment${E}`>( `${config.namespace}/increment${config.entityName}`, ) - const decrementAction = createAction( + const decrementAction = createAction<{ id: number }, `${N}/decrement${E}`>( `${config.namespace}/decrement${config.entityName}`, ) + const setAction = createAction<{ id: number; value: number }, `${N}/set${E}`>( + `${config.namespace}/set${config.entityName}`, + ) + const reducer = createImmerReducer( (state: Draft, action, database: DatabaseState) => { if (addAction.match(action)) { - config.getState(state)[action.payload] ??= create(database, action.payload, 0) + config.getState(state)[action.payload.id] ??= + config.createEmptyActivatableRatedWithEnhancements(action.payload.id) config.registerOrUnregisterPrerequisitesAsDependencies( RegistrationMethod.Add, state, - config.getPrerequisites(action.payload, database), - config.createIdentifierObject(action.payload), + config.getPrerequisites(action.payload.id, database), + config.createIdentifierObject(action.payload.id), ) } else if (removeAction.match(action)) { config.registerOrUnregisterPrerequisitesAsDependencies( RegistrationMethod.Remove, state, - config.getPrerequisites(action.payload, database), - config.createIdentifierObject(action.payload), + config.getPrerequisites(action.payload.id, database), + config.createIdentifierObject(action.payload.id), ) - delete config.getState(state)[action.payload] + delete config.getState(state)[action.payload.id] } else if (incrementAction.match(action)) { - const entry = (config.getState(state)[action.payload] ??= createInitial(action.payload)) + const entry = (config.getState(state)[action.payload.id] ??= + config.createEmptyActivatableRatedWithEnhancements(action.payload.id)) if (entry.value === undefined) { entry.value = 0 } else { @@ -209,24 +143,29 @@ export const createActivatableRatedWithEnhancementsSlice = < } updateCachedAdventurePoints(entry, database) } else if (decrementAction.match(action)) { - const entry = config.getState(state)[action.payload] + const entry = config.getState(state)[action.payload.id] if (entry !== undefined && entry.value !== undefined && entry.value > 0) { entry.value-- updateCachedAdventurePoints(entry, database) } + } else if (setAction.match(action)) { + const entry = (config.getState(state)[action.payload.id] ??= + config.createEmptyActivatableRatedWithEnhancements(action.payload.id)) + if (action.payload.value >= 0) { + entry.value = action.payload.value + updateCachedAdventurePoints(entry, database) + } } }, ) return { - create, - createInitial, - getValue, actions: { addAction, removeAction, incrementAction, decrementAction, + setAction, }, reducer, } diff --git a/src/main_window/slices/attributesSlice.ts b/src/main_window/slices/attributesSlice.ts index 75c6b0b5..c772bf60 100644 --- a/src/main_window/slices/attributesSlice.ts +++ b/src/main_window/slices/attributesSlice.ts @@ -1,25 +1,22 @@ -import { ImprovementCost } from "../../shared/domain/adventurePoints/improvementCost.ts" +import { + attributeImprovementCost, + createEmptyDynamicAttribute, +} from "../../shared/domain/rated/attribute.ts" import { createRatedSlice } from "./ratedSlice.ts" -const { - create, - createInitial, - getValue, - actions: { incrementAction, decrementAction }, - reducer, +// eslint-disable-next-line jsdoc/require-jsdoc +export const { + actions: { + incrementEntry: incrementAttribute, + setEntry: setAttribute, + decrementEntry: decrementAttribute, + }, + reducer: attributesReducer, } = createRatedSlice({ namespace: "attributes", entityName: "Attribute", getState: state => state.attributes, minValue: 8, - getImprovementCost: () => ImprovementCost.E, + getImprovementCost: () => attributeImprovementCost, + createEmptyRated: createEmptyDynamicAttribute, }) - -export { - getValue as attributeValue, - reducer as attributesReducer, - create as createDynamicAttribute, - createInitial as createInitialDynamicAttribute, - decrementAction as decrementAttribute, - incrementAction as incrementAttribute, -} diff --git a/src/main_window/slices/ceremoniesSlice.ts b/src/main_window/slices/ceremoniesSlice.ts index 12723803..f5da6ad9 100644 --- a/src/main_window/slices/ceremoniesSlice.ts +++ b/src/main_window/slices/ceremoniesSlice.ts @@ -1,18 +1,17 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" import { registerOrUnregisterPrerequisitesOfLiturgyAsDependencies } from "../../shared/domain/dependencies/fullPrerequisiteRegistrationAsDependencyForType.ts" import { createIdentifierObject } from "../../shared/domain/identifier.ts" +import { createEmptyDynamicLiturgicalChant } from "../../shared/domain/rated/liturgicalChant.ts" import { createActivatableRatedWithEnhancementsSlice } from "./activatableRatedWithEnhancementsSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicCeremony, - createInitial: createInitialDynamicCeremony, - getValue: getCeremonyValue, actions: { addAction: addCeremony, removeAction: removeCeremony, incrementAction: incrementCeremony, decrementAction: decrementCeremony, + setAction: setCeremony, }, reducer: ceremoniesReducer, } = createActivatableRatedWithEnhancementsSlice({ @@ -27,4 +26,5 @@ export const { createIdentifierObject: id => createIdentifierObject("Ceremony", id), registerOrUnregisterPrerequisitesAsDependencies: registerOrUnregisterPrerequisitesOfLiturgyAsDependencies, + createEmptyActivatableRatedWithEnhancements: createEmptyDynamicLiturgicalChant, }) diff --git a/src/main_window/slices/characterSlice.ts b/src/main_window/slices/characterSlice.ts index ec418807..dd318cd7 100644 --- a/src/main_window/slices/characterSlice.ts +++ b/src/main_window/slices/characterSlice.ts @@ -1,12 +1,12 @@ import { AnyAction, createAction } from "@reduxjs/toolkit" -import { Draft } from "immer" +import { createDynamicActivatable } from "../../shared/domain/activatable/activatableEntry.ts" import { Character } from "../../shared/domain/character.ts" import { AdvantageIdentifier, BlessedTraditionIdentifier, MagicalTraditionIdentifier, } from "../../shared/domain/identifier.ts" -import { createImmerReducer, reduceReducers } from "../../shared/utils/redux.ts" +import { DraftReducer, reduceDraftReducers } from "../../shared/utils/redux.ts" import { RootState } from "../store.ts" import { advantagesReducer } from "./advantagesSlice.ts" import { attributesReducer } from "./attributesSlice.ts" @@ -26,6 +26,7 @@ import { geodeRitualsReducer } from "./magicalActions/geodeRitualsSlice.ts" import { jesterTricksReducer } from "./magicalActions/jesterTricksSlice.ts" import { magicalDancesReducer } from "./magicalActions/magicalDancesSlice.ts" import { magicalMelodiesReducer } from "./magicalActions/magicalMelodiesSlice.ts" +import { magicalRunesReducer } from "./magicalActions/magicalRunesSlice.ts" import { zibiljaRitualsReducer } from "./magicalActions/zibiljaRitualsSlice.ts" import { personalDataReducer } from "./personalDataSlice.ts" import { professionReducer } from "./professionSlice.ts" @@ -53,10 +54,12 @@ const staticInitialState: Omit selectCurrentCharacter(state)?.specialAbilities.lycantropicGifts -/** - * Select the magical runes of the currently open character. - */ -export const selectDynamicMagicalRunes = (state: RootState) => - selectCurrentCharacter(state)?.specialAbilities.magicalRunes - /** * Select the magical signs of the currently open character. */ @@ -932,6 +1014,12 @@ export const selectDynamicGeodeRituals = (state: RootState) => export const selectDynamicZibiljaRituals = (state: RootState) => selectCurrentCharacter(state)?.magicalActions.zibiljaRituals +/** + * Select the magical runes of the currently open character. + */ +export const selectDynamicMagicalRunes = (state: RootState) => + selectCurrentCharacter(state)?.magicalActions.magicalRunes + /** * Select the blessings of the currently open character. */ @@ -986,7 +1074,7 @@ export const deleteAvatar = createAction("character/deleteAvatar") */ export const finishCharacterCreation = createAction("character/finishCharacterCreation") -const generalCharacterReducer = createImmerReducer((state: Draft, action) => { +const generalCharacterReducer: DraftReducer = (state, action) => { if (setName.match(action)) { state.name = action.payload } else if (setAvatar.match(action)) { @@ -996,13 +1084,13 @@ const generalCharacterReducer = createImmerReducer((state: Draft } else if (finishCharacterCreation.match(action)) { state.isCharacterCreationFinished = true } -}) +} /** * The state reducer for a character. */ -export const characterReducer = reduceReducers< - Draft, +export const characterReducer = reduceDraftReducers< + CharacterState, AnyAction, [database: DatabaseState] >( @@ -1030,6 +1118,7 @@ export const characterReducer = reduceReducers< animistPowersReducer, geodeRitualsReducer, zibiljaRitualsReducer, + magicalRunesReducer, blessingsReducer, liturgicalChantsReducer, ceremoniesReducer, diff --git a/src/main_window/slices/closeCombatTechniqueSlice.ts b/src/main_window/slices/closeCombatTechniqueSlice.ts index 90687eb2..a635d4b2 100644 --- a/src/main_window/slices/closeCombatTechniqueSlice.ts +++ b/src/main_window/slices/closeCombatTechniqueSlice.ts @@ -1,12 +1,15 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicCombatTechnique } from "../../shared/domain/rated/combatTechnique.ts" import { createRatedSlice } from "./ratedSlice.ts" -const { - create, - createInitial, - getValue, - actions: { incrementAction, decrementAction }, - reducer, +// eslint-disable-next-line jsdoc/require-jsdoc +export const { + actions: { + incrementEntry: incrementCloseCombatTechnique, + setEntry: setCloseCombatTechnique, + decrementEntry: decrementCloseCombatTechnique, + }, + reducer: closeCombatTechniquesReducer, } = createRatedSlice({ namespace: "combatTechniques/close", entityName: "CloseCombatTechnique", @@ -14,13 +17,5 @@ const { minValue: 6, getImprovementCost: (id, database) => fromRaw(database.closeCombatTechniques[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyRated: createEmptyDynamicCombatTechnique, }) - -export { - reducer as closeCombatTechniquesReducer, - create as createDynamicCloseCombatTechnique, - createInitial as createInitialDynamicCloseCombatTechnique, - decrementAction as decrementCloseCombatTechnique, - getValue as getCloseCombatTechniqueValue, - incrementAction as incrementCloseCombatTechnique, -} diff --git a/src/main_window/slices/databaseSlice.ts b/src/main_window/slices/databaseSlice.ts index afb428d1..be98875e 100644 --- a/src/main_window/slices/databaseSlice.ts +++ b/src/main_window/slices/databaseSlice.ts @@ -45,6 +45,78 @@ const initialDatabaseState: DatabaseState = { brews: {}, cantrips: {}, cache: { + activatableSelectOptions: { + advancedCombatSpecialAbilities: {}, + advancedKarmaSpecialAbilities: {}, + advancedMagicalSpecialAbilities: {}, + advancedSkillSpecialAbilities: {}, + advantages: {}, + ancestorGlyphs: {}, + arcaneOrbEnchantments: {}, + attireEnchantments: {}, + blessedTraditions: {}, + bowlEnchantments: {}, + brawlingSpecialAbilities: {}, + cauldronEnchantments: {}, + ceremonialItemSpecialAbilities: {}, + chronicleEnchantments: {}, + combatSpecialAbilities: {}, + combatStyleSpecialAbilities: {}, + commandSpecialAbilities: {}, + daggerRituals: {}, + disadvantages: {}, + familiarSpecialAbilities: {}, + fatePointSexSpecialAbilities: {}, + fatePointSpecialAbilities: {}, + foolsHatEnchantments: {}, + generalSpecialAbilities: {}, + instrumentEnchantments: {}, + karmaSpecialAbilities: {}, + krallenkettenzauber: {}, + liturgicalStyleSpecialAbilities: {}, + lycantropicGifts: {}, + magicalSpecialAbilities: {}, + magicalTraditions: {}, + magicStyleSpecialAbilities: {}, + orbEnchantments: {}, + pactGifts: {}, + protectiveWardingCircleSpecialAbilities: {}, + ringEnchantments: {}, + sermons: {}, + sexSpecialAbilities: {}, + sickleRituals: {}, + sikaryanDrainSpecialAbilities: {}, + staffEnchantments: {}, + skillStyleSpecialAbilities: {}, + spellSwordEnchantments: {}, + toyEnchantments: {}, + trinkhornzauber: {}, + vampiricGifts: {}, + visions: {}, + wandEnchantments: {}, + weaponEnchantments: {}, + }, + ancestorBloodAdvantages: { + ids: [], + }, + magicalAndBlessedAdvantagesAndDisadvantages: { + advantages: { + magical: { + ids: [], + }, + blessed: { + ids: [], + }, + }, + disadvantages: { + magical: { + ids: [], + }, + blessed: { + ids: [], + }, + }, + }, newApplicationsAndUses: { newApplications: {}, uses: {}, @@ -614,4 +686,14 @@ export const selectStaticWeaponEnchantments = (state: RootState) => export const selectStaticWeapons = (state: RootState) => state.database.weapons export const selectStaticZibiljaRituals = (state: RootState) => state.database.zibiljaRituals +export const selectCache = (state: RootState) => state.database.cache +export const selectActivatableSelectOptionsCache = (state: RootState) => + state.database.cache.activatableSelectOptions +export const selectAncestorBloodAdvantagesCache = (state: RootState) => + state.database.cache.ancestorBloodAdvantages +export const selectMagicalAndBlessedAdvantagesAndDisadvantagesCache = (state: RootState) => + state.database.cache.magicalAndBlessedAdvantagesAndDisadvantages +export const selectNewApplicationsAndUsesCache = (state: RootState) => + state.database.cache.newApplicationsAndUses + export const databaseReducer = databaseSlice.reducer diff --git a/src/main_window/slices/derivedCharacteristicsSlice.ts b/src/main_window/slices/derivedCharacteristicsSlice.ts index 58eaf0e0..548cac86 100644 --- a/src/main_window/slices/derivedCharacteristicsSlice.ts +++ b/src/main_window/slices/derivedCharacteristicsSlice.ts @@ -1,7 +1,7 @@ /* eslint-disable jsdoc/require-jsdoc */ /* eslint-disable max-len */ import { createAction } from "@reduxjs/toolkit" -import { createImmerReducer } from "../../shared/utils/redux.ts" +import { DraftReducer } from "../../shared/utils/redux.ts" import { CharacterState } from "./characterSlice.ts" export const incrementLifePoints = createAction("derivedCharacteristics/incrementLifePoints") @@ -50,7 +50,7 @@ export const decrementKarmaPointsBoughtBack = createAction( "derivedCharacteristics/decrementKarmaPointsBoughtBack", ) -export const derivedCharacteristicsReducer = createImmerReducer((state, action) => { +export const derivedCharacteristicsReducer: DraftReducer = (state, action) => { if (incrementLifePoints.match(action)) { state.derivedCharacteristics.lifePoints.purchased++ } else if (decrementLifePoints.match(action)) { @@ -106,4 +106,4 @@ export const derivedCharacteristicsReducer = createImmerReducer( state.derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack-- } } -}) +} diff --git a/src/main_window/slices/liturgicalChantsSlice.ts b/src/main_window/slices/liturgicalChantsSlice.ts index 5dc70f27..6234c847 100644 --- a/src/main_window/slices/liturgicalChantsSlice.ts +++ b/src/main_window/slices/liturgicalChantsSlice.ts @@ -1,18 +1,17 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" import { registerOrUnregisterPrerequisitesOfLiturgyAsDependencies } from "../../shared/domain/dependencies/fullPrerequisiteRegistrationAsDependencyForType.ts" import { createIdentifierObject } from "../../shared/domain/identifier.ts" +import { createEmptyDynamicLiturgicalChant } from "../../shared/domain/rated/liturgicalChant.ts" import { createActivatableRatedWithEnhancementsSlice } from "./activatableRatedWithEnhancementsSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicLiturgicalChant, - createInitial: createInitialDynamicLiturgicalChant, - getValue: getLiturgicalChantValue, actions: { addAction: addLiturgicalChant, removeAction: removeLiturgicalChant, incrementAction: incrementLiturgicalChant, decrementAction: decrementLiturgicalChant, + setAction: setLiturgicalChant, }, reducer: liturgicalChantsReducer, } = createActivatableRatedWithEnhancementsSlice({ @@ -27,4 +26,5 @@ export const { createIdentifierObject: id => createIdentifierObject("LiturgicalChant", id), registerOrUnregisterPrerequisitesAsDependencies: registerOrUnregisterPrerequisitesOfLiturgyAsDependencies, + createEmptyActivatableRatedWithEnhancements: createEmptyDynamicLiturgicalChant, }) diff --git a/src/main_window/slices/magicalActions/animistPowersSlice.ts b/src/main_window/slices/magicalActions/animistPowersSlice.ts index 669297c3..45ce14e3 100644 --- a/src/main_window/slices/magicalActions/animistPowersSlice.ts +++ b/src/main_window/slices/magicalActions/animistPowersSlice.ts @@ -1,36 +1,39 @@ -import { ImprovementCost, fromRaw } from "../../../shared/domain/adventurePoints/improvementCost.ts" -import { assertExhaustive } from "../../../shared/utils/typeSafety.ts" +import { ImprovementCost } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { MagicalTraditionIdentifier } from "../../../shared/domain/identifier.ts" +import { getImprovementCostForAnimistPower } from "../../../shared/domain/rated/animistPower.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicAnimistPower, - createInitial: createInitialDynamicAnimistPower, - getValue: getAnimistPowerValue, actions: { addAction: addAnimistPower, removeAction: removeAnimistPower, incrementAction: incrementAnimistPower, decrementAction: decrementAnimistPower, + setAction: setAnimistPower, }, reducer: animistPowersReducer, } = createActivatableRatedSlice({ namespace: "animistPowers", entityName: "AnimistPower", getState: state => state.magicalActions.animistPowers, - getImprovementCost: (id, database) => { + getImprovementCost: (id, database, character) => { const animistPower = database.animistPowers[id] - if (animistPower === undefined) { + const traditionAnimists = + character.specialAbilities.magicalTraditions[MagicalTraditionIdentifier.Animisten] + + if (animistPower === undefined || traditionAnimists === undefined) { return ImprovementCost.D } - switch (animistPower.improvement_cost.tag) { - case "Fixed": - return fromRaw(animistPower.improvement_cost.fixed) - case "ByPrimaryPatron": - // TODO: Replace with derived improvement cost - return ImprovementCost.D - default: - return assertExhaustive(animistPower.improvement_cost) - } + + return ( + getImprovementCostForAnimistPower( + patronId => database.patrons[patronId], + traditionAnimists, + animistPower.improvement_cost, + ) ?? ImprovementCost.D + ) }, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/cursesSlice.ts b/src/main_window/slices/magicalActions/cursesSlice.ts index f184a76c..eabaa388 100644 --- a/src/main_window/slices/magicalActions/cursesSlice.ts +++ b/src/main_window/slices/magicalActions/cursesSlice.ts @@ -1,16 +1,17 @@ -import { cursesImprovementCost } from "../../../shared/domain/rated/magicalActions.ts" +import { + createEmptyDynamicMagicalAction, + cursesImprovementCost, +} from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicCurse, - createInitial: createInitialDynamicCurse, - getValue: getCurseValue, actions: { addAction: addCurse, removeAction: removeCurse, incrementAction: incrementCurse, decrementAction: decrementCurse, + setAction: setCurse, }, reducer: cursesReducer, } = createActivatableRatedSlice({ @@ -18,4 +19,5 @@ export const { entityName: "Curse", getState: state => state.magicalActions.curses, getImprovementCost: (_id, _database) => cursesImprovementCost, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/dominationRitualsSlice.ts b/src/main_window/slices/magicalActions/dominationRitualsSlice.ts index 66b6f11a..acc7133c 100644 --- a/src/main_window/slices/magicalActions/dominationRitualsSlice.ts +++ b/src/main_window/slices/magicalActions/dominationRitualsSlice.ts @@ -1,16 +1,17 @@ -import { dominationRitualsImprovementCost } from "../../../shared/domain/rated/magicalActions.ts" +import { + createEmptyDynamicMagicalAction, + dominationRitualsImprovementCost, +} from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicDominationRitual, - createInitial: createInitialDynamicDominationRitual, - getValue: getDominationRitualValue, actions: { addAction: addDominationRitual, removeAction: removeDominationRitual, incrementAction: incrementDominationRitual, decrementAction: decrementDominationRitual, + setAction: setDominationRitual, }, reducer: dominationRitualsReducer, } = createActivatableRatedSlice({ @@ -18,4 +19,5 @@ export const { entityName: "DominationRitual", getState: state => state.magicalActions.dominationRituals, getImprovementCost: (_id, _database) => dominationRitualsImprovementCost, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/elvenMagicalSongsSlice.ts b/src/main_window/slices/magicalActions/elvenMagicalSongsSlice.ts index 98114927..0d200b29 100644 --- a/src/main_window/slices/magicalActions/elvenMagicalSongsSlice.ts +++ b/src/main_window/slices/magicalActions/elvenMagicalSongsSlice.ts @@ -1,16 +1,15 @@ import { ImprovementCost, fromRaw } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicElvenMagicalSong, - createInitial: createInitialDynamicElvenMagicalSong, - getValue: getElvenMagicalSongValue, actions: { addAction: addElvenMagicalSong, removeAction: removeElvenMagicalSong, incrementAction: incrementElvenMagicalSong, decrementAction: decrementElvenMagicalSong, + setAction: setElvenMagicalSong, }, reducer: elvenMagicalSongsReducer, } = createActivatableRatedSlice({ @@ -19,4 +18,5 @@ export const { getState: state => state.magicalActions.elvenMagicalSongs, getImprovementCost: (id, database) => fromRaw(database.elvenMagicalSongs[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/geodeRitualsSlice.ts b/src/main_window/slices/magicalActions/geodeRitualsSlice.ts index 0ff85f63..9f474c22 100644 --- a/src/main_window/slices/magicalActions/geodeRitualsSlice.ts +++ b/src/main_window/slices/magicalActions/geodeRitualsSlice.ts @@ -1,16 +1,17 @@ -import { geodeRitualsImprovementCost } from "../../../shared/domain/rated/magicalActions.ts" +import { + createEmptyDynamicMagicalAction, + geodeRitualsImprovementCost, +} from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicGeodeRitual, - createInitial: createInitialDynamicGeodeRitual, - getValue: getGeodeRitualValue, actions: { addAction: addGeodeRitual, removeAction: removeGeodeRitual, incrementAction: incrementGeodeRitual, decrementAction: decrementGeodeRitual, + setAction: setGeodeRitual, }, reducer: geodeRitualsReducer, } = createActivatableRatedSlice({ @@ -18,4 +19,5 @@ export const { entityName: "GeodeRitual", getState: state => state.magicalActions.geodeRituals, getImprovementCost: (_id, _database) => geodeRitualsImprovementCost, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/jesterTricksSlice.ts b/src/main_window/slices/magicalActions/jesterTricksSlice.ts index 79bb9fc5..47311c70 100644 --- a/src/main_window/slices/magicalActions/jesterTricksSlice.ts +++ b/src/main_window/slices/magicalActions/jesterTricksSlice.ts @@ -1,16 +1,15 @@ import { ImprovementCost, fromRaw } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicJesterTrick, - createInitial: createInitialDynamicJesterTrick, - getValue: getJesterTrickValue, actions: { addAction: addJesterTrick, removeAction: removeJesterTrick, incrementAction: incrementJesterTrick, decrementAction: decrementJesterTrick, + setAction: setJesterTrick, }, reducer: jesterTricksReducer, } = createActivatableRatedSlice({ @@ -19,4 +18,5 @@ export const { getState: state => state.magicalActions.jesterTricks, getImprovementCost: (id, database) => fromRaw(database.jesterTricks[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/magicalDancesSlice.ts b/src/main_window/slices/magicalActions/magicalDancesSlice.ts index 94debb08..317e261a 100644 --- a/src/main_window/slices/magicalActions/magicalDancesSlice.ts +++ b/src/main_window/slices/magicalActions/magicalDancesSlice.ts @@ -1,16 +1,15 @@ import { ImprovementCost, fromRaw } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicMagicalDance, - createInitial: createInitialDynamicMagicalDance, - getValue: getMagicalDanceValue, actions: { addAction: addMagicalDance, removeAction: removeMagicalDance, incrementAction: incrementMagicalDance, decrementAction: decrementMagicalDance, + setAction: setMagicalDance, }, reducer: magicalDancesReducer, } = createActivatableRatedSlice({ @@ -19,4 +18,5 @@ export const { getState: state => state.magicalActions.magicalDances, getImprovementCost: (id, database) => fromRaw(database.magicalDances[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/magicalMelodiesSlice.ts b/src/main_window/slices/magicalActions/magicalMelodiesSlice.ts index c75ae795..dc061a83 100644 --- a/src/main_window/slices/magicalActions/magicalMelodiesSlice.ts +++ b/src/main_window/slices/magicalActions/magicalMelodiesSlice.ts @@ -1,16 +1,15 @@ import { ImprovementCost, fromRaw } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicMagicalMelody, - createInitial: createInitialDynamicMagicalMelody, - getValue: getMagicalMelodyValue, actions: { addAction: addMagicalMelody, removeAction: removeMagicalMelody, incrementAction: incrementMagicalMelody, decrementAction: decrementMagicalMelody, + setAction: setMagicalMelody, }, reducer: magicalMelodiesReducer, } = createActivatableRatedSlice({ @@ -19,4 +18,5 @@ export const { getState: state => state.magicalActions.magicalMelodies, getImprovementCost: (id, database) => fromRaw(database.magicalMelodies[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/magicalActions/magicalRunesSlice.ts b/src/main_window/slices/magicalActions/magicalRunesSlice.ts new file mode 100644 index 00000000..dfc46b17 --- /dev/null +++ b/src/main_window/slices/magicalActions/magicalRunesSlice.ts @@ -0,0 +1,30 @@ +import { ImprovementCost } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" +import { getImprovementCostForMagicalRune } from "../../../shared/domain/rated/magicalRune.ts" +import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" + +// eslint-disable-next-line jsdoc/require-jsdoc +export const { + actions: { + addAction: addMagicalRune, + removeAction: removeMagicalRune, + incrementAction: incrementMagicalRune, + decrementAction: decrementMagicalRune, + setAction: setMagicalRune, + }, + reducer: magicalRunesReducer, +} = createActivatableRatedSlice({ + namespace: "magicalRunes", + entityName: "MagicalRune", + getState: state => state.magicalActions.magicalRunes, + getImprovementCost: (id, database) => { + const staticEntry = database.magicalRunes[id] + return ( + getImprovementCostForMagicalRune( + // staticEntry?.options, + staticEntry?.improvement_cost ?? { tag: "Constant", constant: { value: "D" } }, + ) ?? ImprovementCost.D + ) + }, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, +}) diff --git a/src/main_window/slices/magicalActions/zibiljaRitualsSlice.ts b/src/main_window/slices/magicalActions/zibiljaRitualsSlice.ts index e72bd275..6baa5a6b 100644 --- a/src/main_window/slices/magicalActions/zibiljaRitualsSlice.ts +++ b/src/main_window/slices/magicalActions/zibiljaRitualsSlice.ts @@ -1,16 +1,15 @@ import { ImprovementCost, fromRaw } from "../../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicMagicalAction } from "../../../shared/domain/rated/magicalActions.ts" import { createActivatableRatedSlice } from "../activatableRatedSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicZibiljaRitual, - createInitial: createInitialDynamicZibiljaRitual, - getValue: getZibiljaRitualValue, actions: { addAction: addZibiljaRitual, removeAction: removeZibiljaRitual, incrementAction: incrementZibiljaRitual, decrementAction: decrementZibiljaRitual, + setAction: setZibiljaRitual, }, reducer: zibiljaRitualsReducer, } = createActivatableRatedSlice({ @@ -19,4 +18,5 @@ export const { getState: state => state.magicalActions.zibiljaRituals, getImprovementCost: (id, database) => fromRaw(database.zibiljaRituals[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyActivatableRated: createEmptyDynamicMagicalAction, }) diff --git a/src/main_window/slices/personalDataSlice.ts b/src/main_window/slices/personalDataSlice.ts index 6adc0a52..0302e9c4 100644 --- a/src/main_window/slices/personalDataSlice.ts +++ b/src/main_window/slices/personalDataSlice.ts @@ -4,7 +4,7 @@ import { Height, Weight, WeightDiceOffsetStrategy } from "optolith-database-sche import { DieType } from "optolith-database-schema/types/_Dice" import { rollDice, separateDice } from "../../shared/utils/dice.ts" import { even, parseInt, randomInt } from "../../shared/utils/math.ts" -import { createImmerReducer } from "../../shared/utils/redux.ts" +import { DraftReducer } from "../../shared/utils/redux.ts" import { assertExhaustive } from "../../shared/utils/typeSafety.ts" import { CharacterState } from "./characterSlice.ts" @@ -78,7 +78,7 @@ export const rerollWeight = createAction( }), ) -export const personalDataReducer = createImmerReducer((state, action) => { +export const personalDataReducer: DraftReducer = (state, action) => { if (setFamily.match(action)) { state.personalData.family = action.payload === "" ? undefined : action.payload } else if (setPlaceOfBirth.match(action)) { @@ -131,4 +131,4 @@ export const personalDataReducer = createImmerReducer((state, ac } else if (setOtherInfo.match(action)) { state.personalData.otherInfo = action.payload === "" ? undefined : action.payload } -}) +} diff --git a/src/main_window/slices/professionSlice.ts b/src/main_window/slices/professionSlice.ts index 2c8267c8..5486275c 100644 --- a/src/main_window/slices/professionSlice.ts +++ b/src/main_window/slices/professionSlice.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/require-jsdoc */ import { createAction } from "@reduxjs/toolkit" -import { createImmerReducer } from "../../shared/utils/redux.ts" +import { DraftReducer } from "../../shared/utils/redux.ts" import { CharacterState } from "./characterSlice.ts" export const setProfession = createAction<{ id: number; instanceId: number }>( @@ -9,7 +9,7 @@ export const setProfession = createAction<{ id: number; instanceId: number }>( export const setProfessionVariant = createAction("profession/setProfessionVariant") export const setCustomProfessionName = createAction("profession/setCustomProfessionName") -export const professionReducer = createImmerReducer((state, action) => { +export const professionReducer: DraftReducer = (state, action) => { if (setProfession.match(action)) { state.profession.id = action.payload.id state.profession.instanceId = action.payload.instanceId @@ -19,4 +19,4 @@ export const professionReducer = createImmerReducer((state, acti } else if (setCustomProfessionName.match(action)) { state.profession.customName = action.payload.length > 0 ? action.payload : undefined } -}) +} diff --git a/src/main_window/slices/raceSlice.ts b/src/main_window/slices/raceSlice.ts index 6d395d94..0484ab64 100644 --- a/src/main_window/slices/raceSlice.ts +++ b/src/main_window/slices/raceSlice.ts @@ -1,12 +1,12 @@ /* eslint-disable jsdoc/require-jsdoc */ import { createAction } from "@reduxjs/toolkit" -import { createImmerReducer } from "../../shared/utils/redux.ts" +import { DraftReducer } from "../../shared/utils/redux.ts" import { CharacterState } from "./characterSlice.ts" export const changeAttributeAdjustmentId = createAction("race/changeAttributeAdjustmentId") -export const raceReducer = createImmerReducer((state, action) => { +export const raceReducer: DraftReducer = (state, action) => { if (changeAttributeAdjustmentId.match(action)) { state.race.selectedAttributeAdjustmentId = action.payload } -}) +} diff --git a/src/main_window/slices/rangedCombatTechniqueSlice.ts b/src/main_window/slices/rangedCombatTechniqueSlice.ts index fe317e37..1f43478f 100644 --- a/src/main_window/slices/rangedCombatTechniqueSlice.ts +++ b/src/main_window/slices/rangedCombatTechniqueSlice.ts @@ -1,12 +1,15 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicCombatTechnique } from "../../shared/domain/rated/combatTechnique.ts" import { createRatedSlice } from "./ratedSlice.ts" -const { - create, - createInitial, - getValue, - actions: { incrementAction, decrementAction }, - reducer, +// eslint-disable-next-line jsdoc/require-jsdoc +export const { + actions: { + incrementEntry: incrementRangedCombatTechnique, + setEntry: setRangedCombatTechnique, + decrementEntry: decrementRangedCombatTechnique, + }, + reducer: rangedCombatTechniquesReducer, } = createRatedSlice({ namespace: "combatTechniques/ranged", entityName: "RangedCombatTechnique", @@ -14,13 +17,5 @@ const { minValue: 6, getImprovementCost: (id, database) => fromRaw(database.rangedCombatTechniques[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyRated: createEmptyDynamicCombatTechnique, }) - -export { - create as createDynamicRangedCombatTechnique, - createInitial as createInitialDynamicRangedCombatTechnique, - decrementAction as decrementRangedCombatTechnique, - getValue as getRangedCombatTechniqueValue, - incrementAction as incrementRangedCombatTechnique, - reducer as rangedCombatTechniquesReducer, -} diff --git a/src/main_window/slices/ratedSlice.ts b/src/main_window/slices/ratedSlice.ts index 047187c6..7cff6319 100644 --- a/src/main_window/slices/ratedSlice.ts +++ b/src/main_window/slices/ratedSlice.ts @@ -1,12 +1,8 @@ import { ActionCreatorWithPayload, AnyAction, Draft, createAction } from "@reduxjs/toolkit" import { ImprovementCost } from "../../shared/domain/adventurePoints/improvementCost.ts" -import { - BoundAdventurePoints, - cachedAdventurePoints, -} from "../../shared/domain/adventurePoints/ratedEntry.ts" -import { RatedDependency } from "../../shared/domain/rated/ratedDependency.ts" -import { Rated, RatedMap, RatedValue } from "../../shared/domain/rated/ratedEntry.ts" -import { Reducer, createImmerReducer } from "../../shared/utils/redux.ts" +import { cachedAdventurePoints } from "../../shared/domain/adventurePoints/ratedEntry.ts" +import { Rated, RatedMap } from "../../shared/domain/rated/ratedEntry.ts" +import { DraftReducer } from "../../shared/utils/redux.ts" import { CharacterState } from "./characterSlice.ts" import { DatabaseState } from "./databaseSlice.ts" @@ -14,37 +10,6 @@ import { DatabaseState } from "./databaseSlice.ts" * A slice that handles rated entries. */ export type RatedSlice = { - /** - * Creates a new entry with an initial value if active. The initial adventure - * points cache is calculated from the initial value. - */ - create: ( - database: DatabaseState, - id: number, - value: RatedValue, - options?: Partial<{ - dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] - }>, - ) => Rated - - /** - * Creates a new entry with no initial value. - */ - createInitial: ( - id: number, - options?: Partial<{ - dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] - }>, - ) => Rated - - /** - * Takes an entry that may not exist (because its instance has not been used - * yet) and returns its value. - */ - getValue: (entry: Rated | undefined) => RatedValue - /** * The actions that can be dispatched to modify the state. */ @@ -52,18 +17,23 @@ export type RatedSlice = { /** * Increments the value of the entry with the given id. */ - incrementAction: ActionCreatorWithPayload + incrementEntry: ActionCreatorWithPayload<{ id: number }, `${N}/increment${E}`> + + /** + * Sets the value of the entry with the given id. + */ + setEntry: ActionCreatorWithPayload<{ id: number; value: number }, `${N}/set${E}`> /** * Decrements the value of the entry with the given id. */ - decrementAction: ActionCreatorWithPayload + decrementEntry: ActionCreatorWithPayload<{ id: number }, `${N}/decrement${E}`> } /** * The reducer that handles the actions. */ - reducer: Reducer + reducer: DraftReducer } /** @@ -76,6 +46,7 @@ export const createRatedSlice = (config: { getState: (state: Draft) => Draft minValue: number getImprovementCost: (id: number, database: DatabaseState) => ImprovementCost + createEmptyRated: (id: number) => Rated }): RatedSlice => { const updateCachedAdventurePoints = (entry: Draft, database: DatabaseState) => { entry.cachedAdventurePoints = cachedAdventurePoints( @@ -87,72 +58,49 @@ export const createRatedSlice = (config: { return entry } - const create: RatedSlice["create"] = ( - database, - id, - value, - { dependencies = [], boundAdventurePoints = [] } = {}, - ) => - updateCachedAdventurePoints( - { - id, - value: Math.max(config.minValue, value), - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - }, - database, - ) - - const createInitial: RatedSlice["createInitial"] = ( - id, - { dependencies = [], boundAdventurePoints = [] } = {}, - ) => ({ - id, - value: config.minValue, - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - }) - - const getValue: RatedSlice["getValue"] = entry => entry?.value ?? config.minValue - - const incrementAction = createAction( + const incrementEntry = createAction<{ id: number }, `${N}/increment${E}`>( `${config.namespace}/increment${config.entityName}`, ) - const decrementAction = createAction( + const setEntry = createAction<{ id: number; value: number }, `${N}/set${E}`>( + `${config.namespace}/set${config.entityName}`, + ) + const decrementEntry = createAction<{ id: number }, `${N}/decrement${E}`>( `${config.namespace}/decrement${config.entityName}`, ) - const reducer = createImmerReducer( - (state: Draft, action, database: DatabaseState) => { - if (incrementAction.match(action)) { - const entry = (config.getState(state)[action.payload] ??= createInitial(action.payload)) - entry.value++ + const reducer: DraftReducer = ( + state, + action, + database, + ) => { + if (incrementEntry.match(action)) { + const entry = (config.getState(state)[action.payload.id] ??= config.createEmptyRated( + action.payload.id, + )) + entry.value++ + updateCachedAdventurePoints(entry, database) + } else if (setEntry.match(action)) { + const entry = (config.getState(state)[action.payload.id] ??= config.createEmptyRated( + action.payload.id, + )) + if (action.payload.value >= config.minValue) { + entry.value = action.payload.value updateCachedAdventurePoints(entry, database) - } else if (decrementAction.match(action)) { - const entry = config.getState(state)[action.payload] - if (entry !== undefined && entry.value > config.minValue) { - entry.value-- - updateCachedAdventurePoints(entry, database) - } } - }, - ) + } else if (decrementEntry.match(action)) { + const entry = config.getState(state)[action.payload.id] + if (entry !== undefined && entry.value > config.minValue) { + entry.value-- + updateCachedAdventurePoints(entry, database) + } + } + } return { - create, - createInitial, - getValue, actions: { - incrementAction, - decrementAction, + incrementEntry, + decrementEntry, + setEntry, }, reducer, } diff --git a/src/main_window/slices/ritualsSlice.ts b/src/main_window/slices/ritualsSlice.ts index bb3e43a9..1dc7717f 100644 --- a/src/main_window/slices/ritualsSlice.ts +++ b/src/main_window/slices/ritualsSlice.ts @@ -1,18 +1,17 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" import { registerOrUnregisterPrerequisitesOfSpellworkAsDependencies } from "../../shared/domain/dependencies/fullPrerequisiteRegistrationAsDependencyForType.ts" import { createIdentifierObject } from "../../shared/domain/identifier.ts" +import { createEmptyDynamicSpell } from "../../shared/domain/rated/spell.ts" import { createActivatableRatedWithEnhancementsSlice } from "./activatableRatedWithEnhancementsSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicRitual, - createInitial: createInitialDynamicRitual, - getValue: getRitualValue, actions: { addAction: addRitual, removeAction: removeRitual, incrementAction: incrementRitual, decrementAction: decrementRitual, + setAction: setRitual, }, reducer: ritualsReducer, } = createActivatableRatedWithEnhancementsSlice({ @@ -27,4 +26,5 @@ export const { createIdentifierObject: id => createIdentifierObject("Ritual", id), registerOrUnregisterPrerequisitesAsDependencies: registerOrUnregisterPrerequisitesOfSpellworkAsDependencies, + createEmptyActivatableRatedWithEnhancements: createEmptyDynamicSpell, }) diff --git a/src/main_window/slices/rulesSlice.ts b/src/main_window/slices/rulesSlice.ts index 1f942d47..94d73427 100644 --- a/src/main_window/slices/rulesSlice.ts +++ b/src/main_window/slices/rulesSlice.ts @@ -1,7 +1,7 @@ /* eslint-disable jsdoc/require-jsdoc */ import { createAction } from "@reduxjs/toolkit" import { OptionalRuleIdentifier } from "../../shared/domain/identifier.ts" -import { createImmerReducer } from "../../shared/utils/redux.ts" +import { DraftReducer } from "../../shared/utils/redux.ts" import { CharacterState } from "./characterSlice.ts" export const switchIncludeAllPublications = createAction("rules/switchIncludeAllPublications") @@ -12,7 +12,7 @@ export const changeOptionalRuleOption = createAction<{ id: number; option: numbe "rules/changeOptionalRuleOption", ) -export const rulesReducer = createImmerReducer((state, action) => { +export const rulesReducer: DraftReducer = (state, action) => { if (switchIncludeAllPublications.match(action)) { state.rules.includeAllPublications = !state.rules.includeAllPublications } else if (switchIncludePublication.match(action)) { @@ -54,4 +54,4 @@ export const rulesReducer = createImmerReducer((state, action) = ;(state.rules.optionalRules[action.payload.id]!.options ??= [])[0] = action.payload.option } } -}) +} diff --git a/src/main_window/slices/skillsSlice.ts b/src/main_window/slices/skillsSlice.ts index bbf219e4..b98903de 100644 --- a/src/main_window/slices/skillsSlice.ts +++ b/src/main_window/slices/skillsSlice.ts @@ -1,12 +1,11 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" +import { createEmptyDynamicSkill } from "../../shared/domain/rated/skill.ts" import { createRatedSlice } from "./ratedSlice.ts" -const { - create, - createInitial, - getValue, - actions: { incrementAction, decrementAction }, - reducer, +// eslint-disable-next-line jsdoc/require-jsdoc +export const { + actions: { incrementEntry: incrementSkill, decrementEntry: decrementSkill, setEntry: setSkill }, + reducer: skillsReducer, } = createRatedSlice({ namespace: "skills", entityName: "Skill", @@ -14,13 +13,5 @@ const { minValue: 0, getImprovementCost: (id, database) => fromRaw(database.skills[id]?.improvement_cost) ?? ImprovementCost.D, + createEmptyRated: createEmptyDynamicSkill, }) - -export { - create as createDynamicSkill, - createInitial as createInitialDynamicSkill, - decrementAction as decrementSkill, - incrementAction as incrementSkill, - getValue as skillValue, - reducer as skillsReducer, -} diff --git a/src/main_window/slices/spellsSlice.ts b/src/main_window/slices/spellsSlice.ts index 9b15b6bc..5058c039 100644 --- a/src/main_window/slices/spellsSlice.ts +++ b/src/main_window/slices/spellsSlice.ts @@ -1,18 +1,17 @@ import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" import { registerOrUnregisterPrerequisitesOfSpellworkAsDependencies } from "../../shared/domain/dependencies/fullPrerequisiteRegistrationAsDependencyForType.ts" import { createIdentifierObject } from "../../shared/domain/identifier.ts" +import { createEmptyDynamicSpell } from "../../shared/domain/rated/spell.ts" import { createActivatableRatedWithEnhancementsSlice } from "./activatableRatedWithEnhancementsSlice.ts" // eslint-disable-next-line jsdoc/require-jsdoc export const { - create: createDynamicSpell, - createInitial: createInitialDynamicSpell, - getValue: getSpellValue, actions: { addAction: addSpell, removeAction: removeSpell, incrementAction: incrementSpell, decrementAction: decrementSpell, + setAction: setSpell, }, reducer: spellsReducer, } = createActivatableRatedWithEnhancementsSlice({ @@ -27,4 +26,5 @@ export const { createIdentifierObject: id => createIdentifierObject("Ritual", id), registerOrUnregisterPrerequisitesAsDependencies: registerOrUnregisterPrerequisitesOfSpellworkAsDependencies, + createEmptyActivatableRatedWithEnhancements: createEmptyDynamicSpell, }) diff --git a/src/main_window/slices/tinyActivatableSlice.ts b/src/main_window/slices/tinyActivatableSlice.ts index 76fd881a..35b9d489 100644 --- a/src/main_window/slices/tinyActivatableSlice.ts +++ b/src/main_window/slices/tinyActivatableSlice.ts @@ -14,12 +14,12 @@ export type TinyActivatableSlice = { /** * Adds the entry with the given id. */ - addAction: ActionCreatorWithPayload + addAction: ActionCreatorWithPayload<{ id: number }, `${N}/add${E}`> /** * Remove the entry with the given id. */ - removeAction: ActionCreatorWithPayload + removeAction: ActionCreatorWithPayload<{ id: number }, `${N}/remove${E}`> } /** @@ -36,25 +36,25 @@ export const createTinyActivatableSlice = (c entityName: E getState: (state: Draft) => Draft }): TinyActivatableSlice => { - const addAction = createAction( + const addAction = createAction<{ id: number }, `${N}/add${E}`>( `${config.namespace}/add${config.entityName}`, ) - const removeAction = createAction( + const removeAction = createAction<{ id: number }, `${N}/remove${E}`>( `${config.namespace}/remove${config.entityName}`, ) const reducer = createImmerReducer((state: Draft, action) => { const focusedState = config.getState(state) if (addAction.match(action)) { - if (!Object.hasOwn(focusedState, action.payload)) { - focusedState[action.payload] = { - id: action.payload, + if (!Object.hasOwn(focusedState, action.payload.id)) { + focusedState[action.payload.id] = { + id: action.payload.id, active: true, } } } else if (removeAction.match(action)) { - if (Object.hasOwn(focusedState, action.payload)) { - delete focusedState[action.payload] + if (Object.hasOwn(focusedState, action.payload.id)) { + delete focusedState[action.payload.id] } } }) diff --git a/src/shared/components/recommendedReference/RecommendedReference.tsx b/src/shared/components/recommendedReference/RecommendedReference.tsx index 7ac5a983..c9e2dc39 100644 --- a/src/shared/components/recommendedReference/RecommendedReference.tsx +++ b/src/shared/components/recommendedReference/RecommendedReference.tsx @@ -21,7 +21,7 @@ export const RecommendedReference: FC = props => {
-
{translate("showfrequency.unfamiliarspells")}
+
{translate("Unfamiliar Spells")}
) @@ -32,16 +32,16 @@ export const RecommendedReference: FC = props => { {strongly === true ? (
-
{translate("showfrequency.stronglyrecommended")}
+
{translate("Strongly Recommended")}
) : null}
-
{translate("showfrequency.common")}
+
{translate("Common")}
-
{translate("showfrequency.uncommon")}
+
{translate("Uncommon")}
) diff --git a/src/shared/domain/adventurePoints/ratedEntry.test.ts b/src/shared/domain/adventurePoints/ratedEntry.test.ts index 53420262..2a7e87e4 100644 --- a/src/shared/domain/adventurePoints/ratedEntry.test.ts +++ b/src/shared/domain/adventurePoints/ratedEntry.test.ts @@ -1,11 +1,12 @@ import assert from "node:assert/strict" import { describe, it } from "node:test" +import { AdventurePointsCache } from "./cache.ts" import { ImprovementCost } from "./improvementCost.ts" -import { RatedAdventurePointsCache, cachedAdventurePoints } from "./ratedEntry.ts" +import { cachedAdventurePoints } from "./ratedEntry.ts" describe("cachedAdventurePoints", () => { it("returns the calculated value if bound adventure points are only granted at at least the current rating", () => { - assert.deepEqual( + assert.deepEqual( cachedAdventurePoints(9, 8, [{ rating: 9, adventurePoints: 10 }], ImprovementCost.E), { general: 15, @@ -15,7 +16,7 @@ describe("cachedAdventurePoints", () => { }) it("returns the split calculated value if bound adventure points have to be considered", () => { - assert.deepEqual( + assert.deepEqual( cachedAdventurePoints(9, 8, [{ rating: 8, adventurePoints: 10 }], ImprovementCost.E), { general: 5, diff --git a/src/shared/domain/adventurePoints/ratedEntry.ts b/src/shared/domain/adventurePoints/ratedEntry.ts index 0a2afd37..76e2aa26 100644 --- a/src/shared/domain/adventurePoints/ratedEntry.ts +++ b/src/shared/domain/adventurePoints/ratedEntry.ts @@ -1,4 +1,9 @@ import { range } from "../../utils/array.ts" +import { + AdventurePointsCache, + addAdventurePointsCaches, + emptyAdventurePointsCache, +} from "./cache.ts" import { ImprovementCost, adventurePointsForIncrement } from "./improvementCost.ts" /** @@ -6,7 +11,7 @@ import { ImprovementCost, adventurePointsForIncrement } from "./improvementCost. * entry. They don’t effect the costs of the rating at the time of granting, so * the rating at which they have been granted is stored as well. */ -export type BoundAdventurePoints = { +export type BoundAdventurePointsForRated = { /** * The rating at which they have been granted. If the adventure points have * been granted when the entry was not active yet, the rating is `undefined`. @@ -19,35 +24,8 @@ export type BoundAdventurePoints = { adventurePoints: number } -/** - * The accumulated used adventure points value of all value increases. It is - * split by used bound and used general adventure points. - */ -export type RatedAdventurePointsCache = { - /** - * The used general adventure points. - */ - general: number - - /** - * The used bound adventure points. - */ - bound: number -} - -/** - * Adds two caches together. - */ -const addCache = ( - cache1: RatedAdventurePointsCache, - cache2: RatedAdventurePointsCache, -): RatedAdventurePointsCache => ({ - general: cache1.general + cache2.general, - bound: cache1.bound + cache2.bound, -}) - const groupBoundAdventurePointsByRating = ( - boundAdventurePoints: BoundAdventurePoints[], + boundAdventurePoints: BoundAdventurePointsForRated[], ): ReadonlyMap => boundAdventurePoints.reduce((map, { rating: boundRating, adventurePoints }) => { const key = boundRating ?? "activation" @@ -60,7 +38,7 @@ const accumulateCache = ( initialApplicableBoundKey: number | "activation", boundByValue: ReadonlyMap, ic: ImprovementCost, -): RatedAdventurePointsCache => { +): AdventurePointsCache => { const { usedGeneral, usedBound } = range(startValue, endValue).reduce( (acc, currentValue) => { const costForStep = adventurePointsForIncrement(ic, currentValue - 1) @@ -97,14 +75,11 @@ const accumulateCache = ( export const cachedAdventurePoints = ( value: number, minValue: number, - boundAdventurePoints: BoundAdventurePoints[], + boundAdventurePoints: BoundAdventurePointsForRated[], ic: ImprovementCost, -): RatedAdventurePointsCache => { +): AdventurePointsCache => { if (minValue >= value) { - return { - general: 0, - bound: 0, - } + return emptyAdventurePointsCache } else { const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints) return accumulateCache(minValue + 1, value, minValue, boundByValue, ic) @@ -118,14 +93,11 @@ export const cachedAdventurePoints = ( */ export const cachedAdventurePointsForActivatable = ( value: number | undefined, - boundAdventurePoints: BoundAdventurePoints[], + boundAdventurePoints: BoundAdventurePointsForRated[], ic: ImprovementCost, -): RatedAdventurePointsCache => { +): AdventurePointsCache => { if (value === undefined) { - return { - general: 0, - bound: 0, - } + return emptyAdventurePointsCache } else { const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints) return accumulateCache(0, value, "activation", boundByValue, ic) @@ -140,23 +112,20 @@ export const cachedAdventurePointsForActivatable = ( */ export const cachedAdventurePointsForActivatableWithEnhancements = ( value: number | undefined, - boundAdventurePoints: BoundAdventurePoints[], + boundAdventurePoints: BoundAdventurePointsForRated[], ic: ImprovementCost, enhancements: number[], getAdventurePointsModifierForEnhancement: (enhancementId: number) => number, -): RatedAdventurePointsCache => { +): AdventurePointsCache => { if (value === undefined) { - return { - general: 0, - bound: 0, - } + return emptyAdventurePointsCache } else { const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints) const enhancementAdventurePoints = enhancements.reduce( (acc, enhancementId) => acc + getAdventurePointsModifierForEnhancement(enhancementId), 0, ) - return addCache(accumulateCache(0, value, "activation", boundByValue, ic), { + return addAdventurePointsCaches(accumulateCache(0, value, "activation", boundByValue, ic), { general: enhancementAdventurePoints, bound: 0, }) diff --git a/src/shared/domain/character.test.ts b/src/shared/domain/character.test.ts index 291b78e2..fc22e74f 100644 --- a/src/shared/domain/character.test.ts +++ b/src/shared/domain/character.test.ts @@ -17,10 +17,12 @@ export const baseCharacter: Character = { id: 1, variantId: 1, selectedAttributeAdjustmentId: 1, + dependencies: [], }, culture: { id: 1, isCulturalPackageApplied: false, + dependencies: [], }, profession: { id: 1, @@ -29,12 +31,14 @@ export const baseCharacter: Character = { rules: { includeAllPublications: false, includePublications: [], + publicationDependencies: [], focusRules: {}, optionalRules: {}, }, states: {}, personalData: { sex: { type: "Male" }, + sexDependencies: [], socialStatus: { dependencies: [], }, @@ -69,7 +73,6 @@ export const baseCharacter: Character = { krallenkettenzauber: {}, liturgicalStyleSpecialAbilities: {}, lycantropicGifts: {}, - magicalRunes: {}, magicalSigns: {}, magicalSpecialAbilities: {}, magicalTraditions: {}, @@ -93,6 +96,8 @@ export const baseCharacter: Character = { weaponEnchantments: {}, }, attributes: {}, + magicalPrimaryAttributeDependencies: [], + blessedPrimaryAttributeDependencies: [], derivedCharacteristics: { lifePoints: { purchased: 0, @@ -127,12 +132,11 @@ export const baseCharacter: Character = { animistPowers: {}, geodeRituals: {}, zibiljaRituals: {}, + magicalRunes: {}, }, blessings: [], liturgicalChants: {}, ceremonies: {}, - magicalPrimaryAttributeDependencies: [], - blessedPrimaryAttributeDependencies: [], // items: {} // hitZoneArmors: {} purse: { @@ -143,4 +147,5 @@ export const baseCharacter: Character = { }, // creatures: {} // pact: {} + pactDependencies: [], } diff --git a/src/shared/domain/character.ts b/src/shared/domain/character.ts index f5cd8423..1572366c 100644 --- a/src/shared/domain/character.ts +++ b/src/shared/domain/character.ts @@ -1,9 +1,11 @@ import { ActivatableMap, TinyActivatableMap } from "./activatable/activatableEntry.ts" import { Color } from "./color.ts" +import { CultureDependency } from "./culture.ts" import { Energy, EnergyWithBuyBack } from "./energy.ts" -import { Pact } from "./pact.ts" +import { Pact, PactDependency } from "./pact.ts" import { Purse } from "./purse.ts" -import { RatedDependency } from "./rated/ratedDependency.ts" +import { RaceDependency } from "./race.ts" +import { PrimaryAttributeDependency } from "./rated/primaryAttribute.ts" import { ActivatableRatedMap, ActivatableRatedWithEnhancementsMap, @@ -11,8 +13,9 @@ import { } from "./rated/ratedEntry.ts" import { FocusRuleInstance } from "./rules/focusRule.ts" import { OptionalRuleInstance } from "./rules/optionalRule.ts" -import { Sex } from "./sex.ts" +import { Sex, SexDependency } from "./sex.ts" import { SocialStatusDependency } from "./socialStatus.ts" +import { PublicationDependency } from "./sources/publicationDependency.ts" import { StateInstance } from "./state.ts" /** @@ -81,6 +84,11 @@ export type Character = { * The identifier of the attribute adjustment that has been selected from the race. */ selectedAttributeAdjustmentId: number + + /** + * Dependencies on the selected race. + */ + dependencies: RaceDependency[] } /** @@ -96,6 +104,11 @@ export type Character = { * Describes whether the cultural package has been applied when creating the character. */ isCulturalPackageApplied: boolean + + /** + * Dependencies on the selected culture. + */ + dependencies: CultureDependency[] } /** @@ -152,6 +165,11 @@ export type Character = { */ includePublications: number[] + /** + * Dependencies on certain publications. + */ + publicationDependencies: PublicationDependency[] + /** * A map of focus rules that may be active for the character. */ @@ -183,6 +201,11 @@ export type Character = { */ sex: Sex + /** + * Dependencies on the character’s sex. + */ + sexDependencies: SexDependency[] + /** * The family names and/or family members. */ @@ -285,7 +308,6 @@ export type Character = { krallenkettenzauber: ActivatableMap liturgicalStyleSpecialAbilities: ActivatableMap lycantropicGifts: ActivatableMap - magicalRunes: ActivatableMap magicalSigns: ActivatableMap magicalSpecialAbilities: ActivatableMap magicalTraditions: ActivatableMap @@ -311,6 +333,9 @@ export type Character = { attributes: RatedMap + magicalPrimaryAttributeDependencies: PrimaryAttributeDependency[] + blessedPrimaryAttributeDependencies: PrimaryAttributeDependency[] + derivedCharacteristics: { lifePoints: Energy arcaneEnergy: EnergyWithBuyBack @@ -338,18 +363,17 @@ export type Character = { animistPowers: ActivatableRatedMap geodeRituals: ActivatableRatedMap zibiljaRituals: ActivatableRatedMap + magicalRunes: ActivatableRatedMap } blessings: TinyActivatableMap liturgicalChants: ActivatableRatedWithEnhancementsMap ceremonies: ActivatableRatedWithEnhancementsMap - magicalPrimaryAttributeDependencies: RatedDependency[] - blessedPrimaryAttributeDependencies: RatedDependency[] - // items: {} // hitZoneArmors: {} purse: Purse // creatures: {} pact?: Pact + pactDependencies: PactDependency[] } diff --git a/src/shared/domain/culture.ts b/src/shared/domain/culture.ts index 25ffc7c0..ebd38209 100644 --- a/src/shared/domain/culture.ts +++ b/src/shared/domain/culture.ts @@ -1,4 +1,5 @@ import { Culture } from "optolith-database-schema/types/Culture" +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" import { TranslateMap } from "../utils/translate.ts" /** @@ -12,3 +13,29 @@ export const getCulture = (cultures: Record, id: number): Cultu */ export const getFullCultureName = (translateMap: TranslateMap, culture: Culture): string => translateMap(culture.translations)?.name ?? "" + +/** + * A dependency on a culture. + */ +export type CultureDependency = Readonly<{ + /** + * The identifier of the dependency source. + */ + sourceId: ActivatableIdentifier + + /** + * The top-level index of the prerequisite. If the prerequisite is part of a + * group or disjunction, this is the index of the group or disjunction. + */ + index: number + + /** + * Is the source prerequisite part of a prerequisite disjunction? + */ + isPartOfDisjunction: boolean + + /** + * The required culture's identifier. + */ + id: number +}> diff --git a/src/shared/domain/equipment.ts b/src/shared/domain/equipment.ts new file mode 100644 index 00000000..1ac8cd1f --- /dev/null +++ b/src/shared/domain/equipment.ts @@ -0,0 +1,4 @@ +/** + * Returns the carrying capacity of a character with the given strength value. + */ +export const getCarryingCapacity = (strength: number): number => strength * 2 diff --git a/src/shared/domain/identifier.ts b/src/shared/domain/identifier.ts index 2c5f8414..de5f9ab6 100644 --- a/src/shared/domain/identifier.ts +++ b/src/shared/domain/identifier.ts @@ -1,4 +1,5 @@ import * as ID from "optolith-database-schema/types/_Identifier" +import { assertExhaustive } from "../utils/typeSafety.ts" // TODO: Update for new identifier mappings @@ -143,6 +144,17 @@ export enum SkillIdentifier { Clothworking = 59, } +/** + * Used identifiers of skill groups. + */ +export enum SkillGroupIdentifier { + Physical = 1, + Social = 2, + Nature = 3, + Knowledge = 4, + Craft = 5, +} + /** * Used identifiers of advantages. */ @@ -154,28 +166,28 @@ export enum AdvantageIdentifier { Luck = 14, ExceptionalSkill = 16, ExceptionalCombatTechnique = 17, - IncreasedAstralPower = 23, - IncreasedKarmaPoints = 24, - IncreasedLifePoints = 25, - IncreasedSpirit = 26, - IncreasedToughness = 27, - ImmunityToPoison = 28, - ImmunityToDisease = 29, - MagicalAttunement = 32, - Rich = 36, - SociallyAdaptable = 40, - InspireConfidence = 46, - WeaponAptitude = 47, - Spellcaster = 50, - Unyielding = 54, // Eisern - LargeSpellSelection = 58, - HatredOf = 68, - Prediger = 77, - Visionaer = 78, - ZahlreichePredigten = 79, - ZahlreicheVisionen = 80, - LeichterGang = 92, - Einkommen = 99, + IncreasedAstralPower = 20, + IncreasedKarmaPoints = 21, + IncreasedLifePoints = 22, + IncreasedSpirit = 23, + IncreasedToughness = 24, + ImmunityToPoison = 25, + ImmunityToDisease = 26, + MagicalAttunement = 29, + Rich = 33, + SociallyAdaptable = 37, + InspireConfidence = 43, + WeaponAptitude = 44, + Spellcaster = 47, + Unyielding = 51, // Eisern + HatredOf = 55, + LargeSpellSelection = 66, + LeichterGang = 85, + Preacher = 91, + Visionary = 92, + ManySermons = 93, + ManyVisions = 94, + Einkommen = 129, } /** @@ -186,28 +198,28 @@ export enum DisadvantageIdentifier { AfraidOf = 1, Poor = 2, Slow = 4, - NoFlyingBalm = 17, - NoFamiliar = 18, - MagicalRestriction = 24, - DecreasedArcanePower = 26, - DecreasedKarmaPoints = 27, - DecreasedLifePoints = 28, - DecreasedSpirit = 29, - DecreasedToughness = 30, - BadLuck = 31, - PersonalityFlaw = 33, - Principles = 34, - BadHabit = 36, - NegativeTrait = 37, // Schlechte Eigenschaft - Stigma = 45, - Deaf = 47, // Taub - Incompetent = 48, - Obligations = 50, // Verpflichtungen - Maimed = 51, // Verstümmelt + NoFlyingBalm = 14, + NoFamiliar = 15, + MagicalRestriction = 21, + DecreasedArcanePower = 23, + DecreasedKarmaPoints = 24, + DecreasedLifePoints = 25, + DecreasedSpirit = 26, + DecreasedToughness = 27, + BadLuck = 28, + PersonalityFlaw = 30, + Principles = 31, + BadHabit = 33, + NegativeTrait = 34, // Schlechte Eigenschaft + Stigma = 42, + Deaf = 44, // Taub + Incompetent = 45, + Obligations = 47, // Verpflichtungen + Maimed = 48, // Verstümmelt BrittleBones = 56, // Gläsern - SmallSpellSelection = 59, - WenigePredigten = 72, - WenigeVisionen = 73, + SmallSpellSelection = 64, + FewerSermons = 70, + FewerVisions = 71, } /** @@ -228,8 +240,12 @@ export enum CombatSpecialAbilityIdentifier { * Used identifiers of general special abilities. */ export enum GeneralSpecialAbilityIdentifier { + SkillSpecialization = 9, CraftInstruments = 17, Hunter = 18, + Literacy = 27, + Language = 29, + LanguageSpecialization = 30, FireEater = 53, } @@ -239,6 +255,7 @@ export enum GeneralSpecialAbilityIdentifier { export enum MagicalSpecialAbilityIdentifier { PropertyKnowledge = 3, GrosseMeditation = 12, + Adaptation = 18, Imitationszauberei = 51, } @@ -269,6 +286,21 @@ export enum MagicalTraditionIdentifier { Runenschoepfer = 24, } +/** + * Used identifiers of magical special abilities. + */ +export enum MagicStyleSpecialAbilityIdentifier { + ScholarDesMagierkollegsZuHoningen = 24, + MadaschwesternStil = 55, +} + +/** + * Used identifiers of pact gifts. + */ +export enum PactGiftIdentifier { + DunklesAbbildDerBuendnisgabe = 3, +} + /** * Used identifiers of aspects. */ @@ -305,6 +337,18 @@ export enum BlessedTraditionIdentifier { } type TagPropertyOptions = { + Publication: ID.PublicationIdentifier + ExperienceLevel: ID.ExperienceLevelIdentifier + CoreRule: ID.CoreRuleIdentifier + FocusRule: ID.FocusRuleIdentifier + Subject: ID.SubjectIdentifier + OptionalRule: ID.OptionalRuleIdentifier + Race: ID.RaceIdentifier + Culture: ID.CultureIdentifier + Profession: ID.ProfessionIdentifier + ProfessionVariant: ID.ProfessionVariantIdentifier + Curriculum: ID.CurriculumIdentifier + Guideline: ID.GuidelineIdentifier Advantage: ID.AdvantageIdentifier Disadvantage: ID.DisadvantageIdentifier GeneralSpecialAbility: ID.GeneralSpecialAbilityIdentifier @@ -336,6 +380,7 @@ type TagPropertyOptions = { MagicalTradition: ID.MagicalTraditionIdentifier BlessedTradition: ID.BlessedTraditionIdentifier PactGift: ID.PactGiftIdentifier + VampiricGift: ID.VampiricGiftIdentifier SikaryanDrainSpecialAbility: ID.SikaryanDrainSpecialAbilityIdentifier LycantropicGift: ID.LycantropicGiftIdentifier SkillStyleSpecialAbility: ID.SkillStyleSpecialAbilityIdentifier @@ -353,13 +398,25 @@ type TagPropertyOptions = { ChronicleEnchantment: ID.ChronicleEnchantmentIdentifier Krallenkettenzauber: ID.KrallenkettenzauberIdentifier Trinkhornzauber: ID.TrinkhornzauberIdentifier + MagicalRune: ID.MagicalRuneIdentifier + MagicalSign: ID.MagicalSignIdentifier + Language: ID.LanguageIdentifier + Script: ID.ScriptIdentifier + Continent: ID.ContinentIdentifier + SocialStatus: ID.SocialStatusIdentifier Attribute: ID.AttributeIdentifier Skill: ID.SkillIdentifier + SkillGroup: ID.SkillGroupIdentifier CloseCombatTechnique: ID.CloseCombatTechniqueIdentifier RangedCombatTechnique: ID.RangedCombatTechniqueIdentifier - Cantrip: ID.CantripIdentifier Spell: ID.SpellIdentifier Ritual: ID.RitualIdentifier + Cantrip: ID.CantripIdentifier + Property: ID.PropertyIdentifier + LiturgicalChant: ID.LiturgicalChantIdentifier + Ceremony: ID.CeremonyIdentifier + Blessing: ID.BlessingIdentifier + Aspect: ID.AspectIdentifier Curse: ID.CurseIdentifier ElvenMagicalSong: ID.ElvenMagicalSongIdentifier DominationRitual: ID.DominationRitualIdentifier @@ -369,9 +426,12 @@ type TagPropertyOptions = { AnimistPower: ID.AnimistPowerIdentifier GeodeRitual: ID.GeodeRitualIdentifier ZibiljaRitual: ID.ZibiljaRitualIdentifier - Blessing: ID.BlessingIdentifier - LiturgicalChant: ID.LiturgicalChantIdentifier - Ceremony: ID.CeremonyIdentifier + AnimalType: ID.AnimalTypeIdentifier + TargetCategory: ID.TargetCategoryIdentifier + General: ID.GeneralIdentifier + Element: ID.ElementIdentifier + AnimalShapeSize: ID.AnimalShapeSizeIdentifier + Patron: ID.PatronIdentifier Ammunition: ID.AmmunitionIdentifier Animal: ID.AnimalIdentifier AnimalCare: ID.AnimalCareIdentifier @@ -401,44 +461,56 @@ type TagPropertyOptions = { Vehicle: ID.VehicleIdentifier Weapon: ID.WeaponIdentifier WeaponAccessory: ID.WeaponAccessoryIdentifier + Reach: ID.ReachIdentifier + PatronCategory: ID.PatronCategoryIdentifier + PersonalityTrait: ID.PersonalityTraitIdentifier + HairColor: ID.HairColorIdentifier + EyeColor: ID.EyeColorIdentifier + PactCategory: ID.PactCategoryIdentifier + PactDomain: ID.PactDomainIdentifier + AnimistTribe: ID.AnimistTribeIdentifier + Influence: ID.InfluenceIdentifier + Condition: ID.ConditionIdentifier + State: ID.StateIdentifier + Disease: ID.DiseaseIdentifier + SexPractice: ID.SexPracticeIdentifier + TradeSecret: ID.TradeSecretIdentifier + AnimalShape: ID.AnimalShapeIdentifier + ArcaneBardTradition: ID.ArcaneBardTraditionIdentifier + ArcaneDancerTradition: ID.ArcaneDancerTraditionIdentifier } +// prettier-ignore const TagPropertyMap: { [K in keyof TagPropertyOptions]: (id: number) => TagPropertyOptions[K] } = { + Publication: id => ({ tag: "Publication", publication: id }), + ExperienceLevel: id => ({ tag: "ExperienceLevel", experience_level: id }), + CoreRule: id => ({ tag: "CoreRule", core_rule: id }), + FocusRule: id => ({ tag: "FocusRule", focus_rule: id }), + Subject: id => ({ tag: "Subject", subject: id }), + OptionalRule: id => ({ tag: "OptionalRule", optional_rule: id }), + Race: id => ({ tag: "Race", race: id }), + Culture: id => ({ tag: "Culture", culture: id }), + Profession: id => ({ tag: "Profession", profession: id }), + ProfessionVariant: id => ({ tag: "ProfessionVariant", profession_variant: id }), + Curriculum: id => ({ tag: "Curriculum", curriculum: id }), + Guideline: id => ({ tag: "Guideline", guideline: id }), Advantage: id => ({ tag: "Advantage", advantage: id }), Disadvantage: id => ({ tag: "Disadvantage", disadvantage: id }), GeneralSpecialAbility: id => ({ tag: "GeneralSpecialAbility", general_special_ability: id }), - FatePointSpecialAbility: id => ({ - tag: "FatePointSpecialAbility", - fate_point_special_ability: id, - }), + FatePointSpecialAbility: id => ({ tag: "FatePointSpecialAbility", fate_point_special_ability: id }), CombatSpecialAbility: id => ({ tag: "CombatSpecialAbility", combat_special_ability: id }), MagicalSpecialAbility: id => ({ tag: "MagicalSpecialAbility", magical_special_ability: id }), StaffEnchantment: id => ({ tag: "StaffEnchantment", staff_enchantment: id }), FamiliarSpecialAbility: id => ({ tag: "FamiliarSpecialAbility", familiar_special_ability: id }), KarmaSpecialAbility: id => ({ tag: "KarmaSpecialAbility", karma_special_ability: id }), - ProtectiveWardingCircleSpecialAbility: id => ({ - tag: "ProtectiveWardingCircleSpecialAbility", - protective_warding_circle_special_ability: id, - }), - CombatStyleSpecialAbility: id => ({ - tag: "CombatStyleSpecialAbility", - combat_style_special_ability: id, - }), - AdvancedCombatSpecialAbility: id => ({ - tag: "AdvancedCombatSpecialAbility", - advanced_combat_special_ability: id, - }), + ProtectiveWardingCircleSpecialAbility: id => ({ tag: "ProtectiveWardingCircleSpecialAbility", protective_warding_circle_special_ability: id }), + CombatStyleSpecialAbility: id => ({ tag: "CombatStyleSpecialAbility", combat_style_special_ability: id }), + AdvancedCombatSpecialAbility: id => ({ tag: "AdvancedCombatSpecialAbility", advanced_combat_special_ability: id }), CommandSpecialAbility: id => ({ tag: "CommandSpecialAbility", command_special_ability: id }), - MagicStyleSpecialAbility: id => ({ - tag: "MagicStyleSpecialAbility", - magic_style_special_ability: id, - }), - AdvancedMagicalSpecialAbility: id => ({ - tag: "AdvancedMagicalSpecialAbility", - advanced_magical_special_ability: id, - }), + MagicStyleSpecialAbility: id => ({ tag: "MagicStyleSpecialAbility", magic_style_special_ability: id }), + AdvancedMagicalSpecialAbility: id => ({ tag: "AdvancedMagicalSpecialAbility", advanced_magical_special_ability: id }), SpellSwordEnchantment: id => ({ tag: "SpellSwordEnchantment", spell_sword_enchantment: id }), DaggerRitual: id => ({ tag: "DaggerRitual", dagger_ritual: id }), InstrumentEnchantment: id => ({ tag: "InstrumentEnchantment", instrument_enchantment: id }), @@ -447,45 +519,25 @@ const TagPropertyMap: { WandEnchantment: id => ({ tag: "WandEnchantment", wand_enchantment: id }), BrawlingSpecialAbility: id => ({ tag: "BrawlingSpecialAbility", brawling_special_ability: id }), AncestorGlyph: id => ({ tag: "AncestorGlyph", ancestor_glyph: id }), - CeremonialItemSpecialAbility: id => ({ - tag: "CeremonialItemSpecialAbility", - ceremonial_item_special_ability: id, - }), + CeremonialItemSpecialAbility: id => ({ tag: "CeremonialItemSpecialAbility", ceremonial_item_special_ability: id }), Sermon: id => ({ tag: "Sermon", sermon: id }), - LiturgicalStyleSpecialAbility: id => ({ - tag: "LiturgicalStyleSpecialAbility", - liturgical_style_special_ability: id, - }), - AdvancedKarmaSpecialAbility: id => ({ - tag: "AdvancedKarmaSpecialAbility", - advanced_karma_special_ability: id, - }), + LiturgicalStyleSpecialAbility: id => ({ tag: "LiturgicalStyleSpecialAbility", liturgical_style_special_ability: id }), + AdvancedKarmaSpecialAbility: id => ({ tag: "AdvancedKarmaSpecialAbility", advanced_karma_special_ability: id }), Vision: id => ({ tag: "Vision", vision: id }), MagicalTradition: id => ({ tag: "MagicalTradition", magical_tradition: id }), BlessedTradition: id => ({ tag: "BlessedTradition", blessed_tradition: id }), PactGift: id => ({ tag: "PactGift", pact_gift: id }), - SikaryanDrainSpecialAbility: id => ({ - tag: "SikaryanDrainSpecialAbility", - sikaryan_drain_special_ability: id, - }), + VampiricGift: id => ({ tag: "VampiricGift", vampiric_gift: id }), + SikaryanDrainSpecialAbility: id => ({ tag: "SikaryanDrainSpecialAbility", sikaryan_drain_special_ability: id }), LycantropicGift: id => ({ tag: "LycantropicGift", lycantropic_gift: id }), - SkillStyleSpecialAbility: id => ({ - tag: "SkillStyleSpecialAbility", - skill_style_special_ability: id, - }), - AdvancedSkillSpecialAbility: id => ({ - tag: "AdvancedSkillSpecialAbility", - advanced_skill_special_ability: id, - }), + SkillStyleSpecialAbility: id => ({ tag: "SkillStyleSpecialAbility", skill_style_special_ability: id }), + AdvancedSkillSpecialAbility: id => ({ tag: "AdvancedSkillSpecialAbility", advanced_skill_special_ability: id }), ArcaneOrbEnchantment: id => ({ tag: "ArcaneOrbEnchantment", arcane_orb_enchantment: id }), CauldronEnchantment: id => ({ tag: "CauldronEnchantment", cauldron_enchantment: id }), FoolsHatEnchantment: id => ({ tag: "FoolsHatEnchantment", fools_hat_enchantment: id }), ToyEnchantment: id => ({ tag: "ToyEnchantment", toy_enchantment: id }), BowlEnchantment: id => ({ tag: "BowlEnchantment", bowl_enchantment: id }), - FatePointSexSpecialAbility: id => ({ - tag: "FatePointSexSpecialAbility", - fate_point_sex_special_ability: id, - }), + FatePointSexSpecialAbility: id => ({ tag: "FatePointSexSpecialAbility", fate_point_sex_special_ability: id }), SexSpecialAbility: id => ({ tag: "SexSpecialAbility", sex_special_ability: id }), WeaponEnchantment: id => ({ tag: "WeaponEnchantment", weapon_enchantment: id }), SickleRitual: id => ({ tag: "SickleRitual", sickle_ritual: id }), @@ -493,13 +545,25 @@ const TagPropertyMap: { ChronicleEnchantment: id => ({ tag: "ChronicleEnchantment", chronicle_enchantment: id }), Krallenkettenzauber: id => ({ tag: "Krallenkettenzauber", krallenkettenzauber: id }), Trinkhornzauber: id => ({ tag: "Trinkhornzauber", trinkhornzauber: id }), + MagicalRune: id => ({ tag: "MagicalRune", magical_rune: id }), + MagicalSign: id => ({ tag: "MagicalSign", magical_sign: id }), + Language: id => ({ tag: "Language", language: id }), + Script: id => ({ tag: "Script", script: id }), + Continent: id => ({ tag: "Continent", continent: id }), + SocialStatus: id => ({ tag: "SocialStatus", social_status: id }), Attribute: id => ({ tag: "Attribute", attribute: id }), Skill: id => ({ tag: "Skill", skill: id }), + SkillGroup: id => ({ tag: "SkillGroup", skill_group: id }), CloseCombatTechnique: id => ({ tag: "CloseCombatTechnique", close_combat_technique: id }), RangedCombatTechnique: id => ({ tag: "RangedCombatTechnique", ranged_combat_technique: id }), - Cantrip: id => ({ tag: "Cantrip", cantrip: id }), Spell: id => ({ tag: "Spell", spell: id }), Ritual: id => ({ tag: "Ritual", ritual: id }), + Cantrip: id => ({ tag: "Cantrip", cantrip: id }), + Property: id => ({ tag: "Property", property: id }), + LiturgicalChant: id => ({ tag: "LiturgicalChant", liturgical_chant: id }), + Ceremony: id => ({ tag: "Ceremony", ceremony: id }), + Blessing: id => ({ tag: "Blessing", blessing: id }), + Aspect: id => ({ tag: "Aspect", aspect: id }), Curse: id => ({ tag: "Curse", curse: id }), ElvenMagicalSong: id => ({ tag: "ElvenMagicalSong", elven_magical_song: id }), DominationRitual: id => ({ tag: "DominationRitual", domination_ritual: id }), @@ -509,9 +573,12 @@ const TagPropertyMap: { AnimistPower: id => ({ tag: "AnimistPower", animist_power: id }), GeodeRitual: id => ({ tag: "GeodeRitual", geode_ritual: id }), ZibiljaRitual: id => ({ tag: "ZibiljaRitual", zibilja_ritual: id }), - Blessing: id => ({ tag: "Blessing", blessing: id }), - LiturgicalChant: id => ({ tag: "LiturgicalChant", liturgical_chant: id }), - Ceremony: id => ({ tag: "Ceremony", ceremony: id }), + AnimalType: id => ({ tag: "AnimalType", animal_type: id }), + TargetCategory: id => ({ tag: "TargetCategory", target_category: id }), + General: id => ({ tag: "General", general: id }), + Element: id => ({ tag: "Element", element: id }), + AnimalShapeSize: id => ({ tag: "AnimalShapeSize", animal_shape_size: id }), + Patron: id => ({ tag: "Patron", patron: id }), Ammunition: id => ({ tag: "Ammunition", ammunition: id }), Animal: id => ({ tag: "Animal", animal: id }), AnimalCare: id => ({ tag: "AnimalCare", animal_care: id }), @@ -524,14 +591,8 @@ const TagPropertyMap: { Elixir: id => ({ tag: "Elixir", elixir: id }), EquipmentOfBlessedOnes: id => ({ tag: "EquipmentOfBlessedOnes", equipment_of_blessed_ones: id }), GemOrPreciousStone: id => ({ tag: "GemOrPreciousStone", gem_or_precious_stone: id }), - IlluminationLightSource: id => ({ - tag: "IlluminationLightSource", - illumination_light_source: id, - }), - IlluminationRefillsOrSupplies: id => ({ - tag: "IlluminationRefillsOrSupplies", - illumination_refills_or_supplies: id, - }), + IlluminationLightSource: id => ({ tag: "IlluminationLightSource", illumination_light_source: id }), + IlluminationRefillsOrSupplies: id => ({ tag: "IlluminationRefillsOrSupplies", illumination_refills_or_supplies: id }), Jewelry: id => ({ tag: "Jewelry", jewelry: id }), Liebesspielzeug: id => ({ tag: "Liebesspielzeug", liebesspielzeug: id }), LuxuryGood: id => ({ tag: "LuxuryGood", luxury_good: id }), @@ -547,6 +608,23 @@ const TagPropertyMap: { Vehicle: id => ({ tag: "Vehicle", vehicle: id }), Weapon: id => ({ tag: "Weapon", weapon: id }), WeaponAccessory: id => ({ tag: "WeaponAccessory", weapon_accessory: id }), + Reach: id => ({ tag: "Reach", reach: id }), + PatronCategory: id => ({ tag: "PatronCategory", patron_category: id }), + PersonalityTrait: id => ({ tag: "PersonalityTrait", personality_trait: id }), + HairColor: id => ({ tag: "HairColor", hair_color: id }), + EyeColor: id => ({ tag: "EyeColor", eye_color: id }), + PactCategory: id => ({ tag: "PactCategory", pact_category: id }), + PactDomain: id => ({ tag: "PactDomain", pact_domain: id }), + AnimistTribe: id => ({ tag: "AnimistTribe", animist_tribe: id }), + Influence: id => ({ tag: "Influence", influence: id }), + Condition: id => ({ tag: "Condition", condition: id }), + State: id => ({ tag: "State", state: id }), + Disease: id => ({ tag: "Disease", disease: id }), + SexPractice: id => ({ tag: "SexPractice", sex_practice: id }), + TradeSecret: id => ({ tag: "TradeSecret", trade_secret: id }), + AnimalShape: id => ({ tag: "AnimalShape", animal_shape: id }), + ArcaneBardTradition: id => ({ tag: "ArcaneBardTradition", arcane_bard_tradition: id }), + ArcaneDancerTradition: id => ({ tag: "ArcaneDancerTradition", arcane_dancer_tradition: id }), } /** @@ -556,3 +634,471 @@ export const createIdentifierObject = ( tag: T, id: number, ): TagPropertyOptions[T] => TagPropertyMap[tag](id) + +/** + * Returns a function by a type name that creates an identifier object from a + * plain identifier. + */ +export const getCreateIdentifierObject = ( + tag: T, +): ((id: number) => TagPropertyOptions[T]) => TagPropertyMap[tag] + +/** + * Splits an identifier object into its type name and plain identifier. + */ +export const splitIdentifierObject = ( + obj: TagPropertyOptions[T], +): [tag: T, id: number] => { + // prettier-ignore + switch (obj.tag) { + case "Publication": return [obj.tag as T, obj.publication] + case "ExperienceLevel": return [obj.tag as T, obj.experience_level] + case "CoreRule": return [obj.tag as T, obj.core_rule] + case "FocusRule": return [obj.tag as T, obj.focus_rule] + case "Subject": return [obj.tag as T, obj.subject] + case "OptionalRule": return [obj.tag as T, obj.optional_rule] + case "Race": return [obj.tag as T, obj.race] + case "Culture": return [obj.tag as T, obj.culture] + case "Profession": return [obj.tag as T, obj.profession] + case "ProfessionVariant": return [obj.tag as T, obj.profession_variant] + case "Curriculum": return [obj.tag as T, obj.curriculum] + case "Guideline": return [obj.tag as T, obj.guideline] + case "Advantage": return [obj.tag as T, obj.advantage] + case "Disadvantage": return [obj.tag as T, obj.disadvantage] + case "GeneralSpecialAbility": return [obj.tag as T, obj.general_special_ability] + case "FatePointSpecialAbility": return [obj.tag as T, obj.fate_point_special_ability] + case "CombatSpecialAbility": return [obj.tag as T, obj.combat_special_ability] + case "MagicalSpecialAbility": return [obj.tag as T, obj.magical_special_ability] + case "StaffEnchantment": return [obj.tag as T, obj.staff_enchantment] + case "FamiliarSpecialAbility": return [obj.tag as T, obj.familiar_special_ability] + case "KarmaSpecialAbility": return [obj.tag as T, obj.karma_special_ability] + case "ProtectiveWardingCircleSpecialAbility": return [obj.tag as T, obj.protective_warding_circle_special_ability] + case "CombatStyleSpecialAbility": return [obj.tag as T, obj.combat_style_special_ability] + case "AdvancedCombatSpecialAbility": return [obj.tag as T, obj.advanced_combat_special_ability] + case "CommandSpecialAbility": return [obj.tag as T, obj.command_special_ability] + case "MagicStyleSpecialAbility": return [obj.tag as T, obj.magic_style_special_ability] + case "AdvancedMagicalSpecialAbility": return [obj.tag as T, obj.advanced_magical_special_ability] + case "SpellSwordEnchantment": return [obj.tag as T, obj.spell_sword_enchantment] + case "DaggerRitual": return [obj.tag as T, obj.dagger_ritual] + case "InstrumentEnchantment": return [obj.tag as T, obj.instrument_enchantment] + case "AttireEnchantment": return [obj.tag as T, obj.attire_enchantment] + case "OrbEnchantment": return [obj.tag as T, obj.orb_enchantment] + case "WandEnchantment": return [obj.tag as T, obj.wand_enchantment] + case "BrawlingSpecialAbility": return [obj.tag as T, obj.brawling_special_ability] + case "AncestorGlyph": return [obj.tag as T, obj.ancestor_glyph] + case "CeremonialItemSpecialAbility": return [obj.tag as T, obj.ceremonial_item_special_ability] + case "Sermon": return [obj.tag as T, obj.sermon] + case "LiturgicalStyleSpecialAbility": return [obj.tag as T, obj.liturgical_style_special_ability] + case "AdvancedKarmaSpecialAbility": return [obj.tag as T, obj.advanced_karma_special_ability] + case "Vision": return [obj.tag as T, obj.vision] + case "MagicalTradition": return [obj.tag as T, obj.magical_tradition] + case "BlessedTradition": return [obj.tag as T, obj.blessed_tradition] + case "PactGift": return [obj.tag as T, obj.pact_gift] + case "VampiricGift": return [obj.tag as T, obj.vampiric_gift] + case "SikaryanDrainSpecialAbility": return [obj.tag as T, obj.sikaryan_drain_special_ability] + case "LycantropicGift": return [obj.tag as T, obj.lycantropic_gift] + case "SkillStyleSpecialAbility": return [obj.tag as T, obj.skill_style_special_ability] + case "AdvancedSkillSpecialAbility": return [obj.tag as T, obj.advanced_skill_special_ability] + case "ArcaneOrbEnchantment": return [obj.tag as T, obj.arcane_orb_enchantment] + case "CauldronEnchantment": return [obj.tag as T, obj.cauldron_enchantment] + case "FoolsHatEnchantment": return [obj.tag as T, obj.fools_hat_enchantment] + case "ToyEnchantment": return [obj.tag as T, obj.toy_enchantment] + case "BowlEnchantment": return [obj.tag as T, obj.bowl_enchantment] + case "FatePointSexSpecialAbility": return [obj.tag as T, obj.fate_point_sex_special_ability] + case "SexSpecialAbility": return [obj.tag as T, obj.sex_special_ability] + case "WeaponEnchantment": return [obj.tag as T, obj.weapon_enchantment] + case "SickleRitual": return [obj.tag as T, obj.sickle_ritual] + case "RingEnchantment": return [obj.tag as T, obj.ring_enchantment] + case "ChronicleEnchantment": return [obj.tag as T, obj.chronicle_enchantment] + case "Krallenkettenzauber": return [obj.tag as T, obj.krallenkettenzauber] + case "Trinkhornzauber": return [obj.tag as T, obj.trinkhornzauber] + case "MagicalRune": return [obj.tag as T, obj.magical_rune] + case "MagicalSign": return [obj.tag as T, obj.magical_sign] + case "Language": return [obj.tag as T, obj.language] + case "Script": return [obj.tag as T, obj.script] + case "Continent": return [obj.tag as T, obj.continent] + case "SocialStatus": return [obj.tag as T, obj.social_status] + case "Attribute": return [obj.tag as T, obj.attribute] + case "Skill": return [obj.tag as T, obj.skill] + case "SkillGroup": return [obj.tag as T, obj.skill_group] + case "CloseCombatTechnique": return [obj.tag as T, obj.close_combat_technique] + case "RangedCombatTechnique": return [obj.tag as T, obj.ranged_combat_technique] + case "Spell": return [obj.tag as T, obj.spell] + case "Ritual": return [obj.tag as T, obj.ritual] + case "Cantrip": return [obj.tag as T, obj.cantrip] + case "Property": return [obj.tag as T, obj.property] + case "LiturgicalChant": return [obj.tag as T, obj.liturgical_chant] + case "Ceremony": return [obj.tag as T, obj.ceremony] + case "Blessing": return [obj.tag as T, obj.blessing] + case "Aspect": return [obj.tag as T, obj.aspect] + case "Curse": return [obj.tag as T, obj.curse] + case "ElvenMagicalSong": return [obj.tag as T, obj.elven_magical_song] + case "DominationRitual": return [obj.tag as T, obj.domination_ritual] + case "MagicalMelody": return [obj.tag as T, obj.magical_melody] + case "MagicalDance": return [obj.tag as T, obj.magical_dance] + case "JesterTrick": return [obj.tag as T, obj.jester_trick] + case "AnimistPower": return [obj.tag as T, obj.animist_power] + case "GeodeRitual": return [obj.tag as T, obj.geode_ritual] + case "ZibiljaRitual": return [obj.tag as T, obj.zibilja_ritual] + case "AnimalType": return [obj.tag as T, obj.animal_type] + case "TargetCategory": return [obj.tag as T, obj.target_category] + case "General": return [obj.tag as T, obj.general] + case "Element": return [obj.tag as T, obj.element] + case "AnimalShapeSize": return [obj.tag as T, obj.animal_shape_size] + case "Patron": return [obj.tag as T, obj.patron] + case "Ammunition": return [obj.tag as T, obj.ammunition] + case "Animal": return [obj.tag as T, obj.animal] + case "AnimalCare": return [obj.tag as T, obj.animal_care] + case "Armor": return [obj.tag as T, obj.armor] + case "BandageOrRemedy": return [obj.tag as T, obj.bandage_or_remedy] + case "Book": return [obj.tag as T, obj.book] + case "CeremonialItem": return [obj.tag as T, obj.ceremonial_item] + case "Clothes": return [obj.tag as T, obj.clothes] + case "Container": return [obj.tag as T, obj.container] + case "Elixir": return [obj.tag as T, obj.elixir] + case "EquipmentOfBlessedOnes": return [obj.tag as T, obj.equipment_of_blessed_ones] + case "GemOrPreciousStone": return [obj.tag as T, obj.gem_or_precious_stone] + case "IlluminationLightSource": return [obj.tag as T, obj.illumination_light_source] + case "IlluminationRefillsOrSupplies": return [obj.tag as T, obj.illumination_refills_or_supplies] + case "Jewelry": return [obj.tag as T, obj.jewelry] + case "Liebesspielzeug": return [obj.tag as T, obj.liebesspielzeug] + case "LuxuryGood": return [obj.tag as T, obj.luxury_good] + case "MagicalArtifact": return [obj.tag as T, obj.magical_artifact] + case "MusicalInstrument": return [obj.tag as T, obj.musical_instrument] + case "OrienteeringAid": return [obj.tag as T, obj.orienteering_aid] + case "Poison": return [obj.tag as T, obj.poison] + case "RopeOrChain": return [obj.tag as T, obj.rope_or_chain] + case "Stationary": return [obj.tag as T, obj.stationary] + case "ThievesTool": return [obj.tag as T, obj.thieves_tool] + case "ToolOfTheTrade": return [obj.tag as T, obj.tool_of_the_trade] + case "TravelGearOrTool": return [obj.tag as T, obj.travel_gear_or_tool] + case "Vehicle": return [obj.tag as T, obj.vehicle] + case "Weapon": return [obj.tag as T, obj.weapon] + case "WeaponAccessory": return [obj.tag as T, obj.weapon_accessory] + case "Reach": return [obj.tag as T, obj.reach] + case "PatronCategory": return [obj.tag as T, obj.patron_category] + case "PersonalityTrait": return [obj.tag as T, obj.personality_trait] + case "HairColor": return [obj.tag as T, obj.hair_color] + case "EyeColor": return [obj.tag as T, obj.eye_color] + case "PactCategory": return [obj.tag as T, obj.pact_category] + case "PactDomain": return [obj.tag as T, obj.pact_domain] + case "AnimistTribe": return [obj.tag as T, obj.animist_tribe] + case "Influence": return [obj.tag as T, obj.influence] + case "Condition": return [obj.tag as T, obj.condition] + case "State": return [obj.tag as T, obj.state] + case "Disease": return [obj.tag as T, obj.disease] + case "SexPractice": return [obj.tag as T, obj.sex_practice] + case "TradeSecret": return [obj.tag as T, obj.trade_secret] + case "AnimalShape": return [obj.tag as T, obj.animal_shape] + case "ArcaneBardTradition": return [obj.tag as T, obj.arcane_bard_tradition] + case "ArcaneDancerTradition": return [obj.tag as T, obj.arcane_dancer_tradition] + default: return assertExhaustive(obj) + } +} + +/** + * Checks if two identifiers are equal. + */ +export const equalsIdentifier = ( + a: T, + b: T, +): boolean => { + // prettier-ignore + switch (a.tag) { + case "Publication": return b.tag === "Publication" && a.publication === b.publication + case "ExperienceLevel": return b.tag === "ExperienceLevel" && a.experience_level === b.experience_level + case "CoreRule": return b.tag === "CoreRule" && a.core_rule === b.core_rule + case "FocusRule": return b.tag === "FocusRule" && a.focus_rule === b.focus_rule + case "Subject": return b.tag === "Subject" && a.subject === b.subject + case "OptionalRule": return b.tag === "OptionalRule" && a.optional_rule === b.optional_rule + case "Race": return b.tag === "Race" && a.race === b.race + case "Culture": return b.tag === "Culture" && a.culture === b.culture + case "Profession": return b.tag === "Profession" && a.profession === b.profession + case "ProfessionVariant": return b.tag === "ProfessionVariant" && a.profession_variant === b.profession_variant + case "Curriculum": return b.tag === "Curriculum" && a.curriculum === b.curriculum + case "Guideline": return b.tag === "Guideline" && a.guideline === b.guideline + case "Advantage": return b.tag === "Advantage" && a.advantage === b.advantage + case "Disadvantage": return b.tag === "Disadvantage" && a.disadvantage === b.disadvantage + case "GeneralSpecialAbility": return b.tag === "GeneralSpecialAbility" && a.general_special_ability === b.general_special_ability + case "FatePointSpecialAbility": return b.tag === "FatePointSpecialAbility" && a.fate_point_special_ability === b.fate_point_special_ability + case "CombatSpecialAbility": return b.tag === "CombatSpecialAbility" && a.combat_special_ability === b.combat_special_ability + case "MagicalSpecialAbility": return b.tag === "MagicalSpecialAbility" && a.magical_special_ability === b.magical_special_ability + case "StaffEnchantment": return b.tag === "StaffEnchantment" && a.staff_enchantment === b.staff_enchantment + case "FamiliarSpecialAbility": return b.tag === "FamiliarSpecialAbility" && a.familiar_special_ability === b.familiar_special_ability + case "KarmaSpecialAbility": return b.tag === "KarmaSpecialAbility" && a.karma_special_ability === b.karma_special_ability + case "ProtectiveWardingCircleSpecialAbility": return b.tag === "ProtectiveWardingCircleSpecialAbility" && a.protective_warding_circle_special_ability === b.protective_warding_circle_special_ability + case "CombatStyleSpecialAbility": return b.tag === "CombatStyleSpecialAbility" && a.combat_style_special_ability === b.combat_style_special_ability + case "AdvancedCombatSpecialAbility": return b.tag === "AdvancedCombatSpecialAbility" && a.advanced_combat_special_ability === b.advanced_combat_special_ability + case "CommandSpecialAbility": return b.tag === "CommandSpecialAbility" && a.command_special_ability === b.command_special_ability + case "MagicStyleSpecialAbility": return b.tag === "MagicStyleSpecialAbility" && a.magic_style_special_ability === b.magic_style_special_ability + case "AdvancedMagicalSpecialAbility": return b.tag === "AdvancedMagicalSpecialAbility" && a.advanced_magical_special_ability === b.advanced_magical_special_ability + case "SpellSwordEnchantment": return b.tag === "SpellSwordEnchantment" && a.spell_sword_enchantment === b.spell_sword_enchantment + case "DaggerRitual": return b.tag === "DaggerRitual" && a.dagger_ritual === b.dagger_ritual + case "InstrumentEnchantment": return b.tag === "InstrumentEnchantment" && a.instrument_enchantment === b.instrument_enchantment + case "AttireEnchantment": return b.tag === "AttireEnchantment" && a.attire_enchantment === b.attire_enchantment + case "OrbEnchantment": return b.tag === "OrbEnchantment" && a.orb_enchantment === b.orb_enchantment + case "WandEnchantment": return b.tag === "WandEnchantment" && a.wand_enchantment === b.wand_enchantment + case "BrawlingSpecialAbility": return b.tag === "BrawlingSpecialAbility" && a.brawling_special_ability === b.brawling_special_ability + case "AncestorGlyph": return b.tag === "AncestorGlyph" && a.ancestor_glyph === b.ancestor_glyph + case "CeremonialItemSpecialAbility": return b.tag === "CeremonialItemSpecialAbility" && a.ceremonial_item_special_ability === b.ceremonial_item_special_ability + case "Sermon": return b.tag === "Sermon" && a.sermon === b.sermon + case "LiturgicalStyleSpecialAbility": return b.tag === "LiturgicalStyleSpecialAbility" && a.liturgical_style_special_ability === b.liturgical_style_special_ability + case "AdvancedKarmaSpecialAbility": return b.tag === "AdvancedKarmaSpecialAbility" && a.advanced_karma_special_ability === b.advanced_karma_special_ability + case "Vision": return b.tag === "Vision" && a.vision === b.vision + case "MagicalTradition": return b.tag === "MagicalTradition" && a.magical_tradition === b.magical_tradition + case "BlessedTradition": return b.tag === "BlessedTradition" && a.blessed_tradition === b.blessed_tradition + case "PactGift": return b.tag === "PactGift" && a.pact_gift === b.pact_gift + case "VampiricGift": return b.tag === "VampiricGift" && a.vampiric_gift === b.vampiric_gift + case "SikaryanDrainSpecialAbility": return b.tag === "SikaryanDrainSpecialAbility" && a.sikaryan_drain_special_ability === b.sikaryan_drain_special_ability + case "LycantropicGift": return b.tag === "LycantropicGift" && a.lycantropic_gift === b.lycantropic_gift + case "SkillStyleSpecialAbility": return b.tag === "SkillStyleSpecialAbility" && a.skill_style_special_ability === b.skill_style_special_ability + case "AdvancedSkillSpecialAbility": return b.tag === "AdvancedSkillSpecialAbility" && a.advanced_skill_special_ability === b.advanced_skill_special_ability + case "ArcaneOrbEnchantment": return b.tag === "ArcaneOrbEnchantment" && a.arcane_orb_enchantment === b.arcane_orb_enchantment + case "CauldronEnchantment": return b.tag === "CauldronEnchantment" && a.cauldron_enchantment === b.cauldron_enchantment + case "FoolsHatEnchantment": return b.tag === "FoolsHatEnchantment" && a.fools_hat_enchantment === b.fools_hat_enchantment + case "ToyEnchantment": return b.tag === "ToyEnchantment" && a.toy_enchantment === b.toy_enchantment + case "BowlEnchantment": return b.tag === "BowlEnchantment" && a.bowl_enchantment === b.bowl_enchantment + case "FatePointSexSpecialAbility": return b.tag === "FatePointSexSpecialAbility" && a.fate_point_sex_special_ability === b.fate_point_sex_special_ability + case "SexSpecialAbility": return b.tag === "SexSpecialAbility" && a.sex_special_ability === b.sex_special_ability + case "WeaponEnchantment": return b.tag === "WeaponEnchantment" && a.weapon_enchantment === b.weapon_enchantment + case "SickleRitual": return b.tag === "SickleRitual" && a.sickle_ritual === b.sickle_ritual + case "RingEnchantment": return b.tag === "RingEnchantment" && a.ring_enchantment === b.ring_enchantment + case "ChronicleEnchantment": return b.tag === "ChronicleEnchantment" && a.chronicle_enchantment === b.chronicle_enchantment + case "Krallenkettenzauber": return b.tag === "Krallenkettenzauber" && a.krallenkettenzauber === b.krallenkettenzauber + case "Trinkhornzauber": return b.tag === "Trinkhornzauber" && a.trinkhornzauber === b.trinkhornzauber + case "MagicalRune": return b.tag === "MagicalRune" && a.magical_rune === b.magical_rune + case "MagicalSign": return b.tag === "MagicalSign" && a.magical_sign === b.magical_sign + case "Language": return b.tag === "Language" && a.language === b.language + case "Script": return b.tag === "Script" && a.script === b.script + case "Continent": return b.tag === "Continent" && a.continent === b.continent + case "SocialStatus": return b.tag === "SocialStatus" && a.social_status === b.social_status + case "Attribute": return b.tag === "Attribute" && a.attribute === b.attribute + case "Skill": return b.tag === "Skill" && a.skill === b.skill + case "SkillGroup": return b.tag === "SkillGroup" && a.skill_group === b.skill_group + case "CloseCombatTechnique": return b.tag === "CloseCombatTechnique" && a.close_combat_technique === b.close_combat_technique + case "RangedCombatTechnique": return b.tag === "RangedCombatTechnique" && a.ranged_combat_technique === b.ranged_combat_technique + case "Spell": return b.tag === "Spell" && a.spell === b.spell + case "Ritual": return b.tag === "Ritual" && a.ritual === b.ritual + case "Cantrip": return b.tag === "Cantrip" && a.cantrip === b.cantrip + case "Property": return b.tag === "Property" && a.property === b.property + case "LiturgicalChant": return b.tag === "LiturgicalChant" && a.liturgical_chant === b.liturgical_chant + case "Ceremony": return b.tag === "Ceremony" && a.ceremony === b.ceremony + case "Blessing": return b.tag === "Blessing" && a.blessing === b.blessing + case "Aspect": return b.tag === "Aspect" && a.aspect === b.aspect + case "Curse": return b.tag === "Curse" && a.curse === b.curse + case "ElvenMagicalSong": return b.tag === "ElvenMagicalSong" && a.elven_magical_song === b.elven_magical_song + case "DominationRitual": return b.tag === "DominationRitual" && a.domination_ritual === b.domination_ritual + case "MagicalMelody": return b.tag === "MagicalMelody" && a.magical_melody === b.magical_melody + case "MagicalDance": return b.tag === "MagicalDance" && a.magical_dance === b.magical_dance + case "JesterTrick": return b.tag === "JesterTrick" && a.jester_trick === b.jester_trick + case "AnimistPower": return b.tag === "AnimistPower" && a.animist_power === b.animist_power + case "GeodeRitual": return b.tag === "GeodeRitual" && a.geode_ritual === b.geode_ritual + case "ZibiljaRitual": return b.tag === "ZibiljaRitual" && a.zibilja_ritual === b.zibilja_ritual + case "AnimalType": return b.tag === "AnimalType" && a.animal_type === b.animal_type + case "TargetCategory": return b.tag === "TargetCategory" && a.target_category === b.target_category + case "General": return b.tag === "General" && a.general === b.general + case "Element": return b.tag === "Element" && a.element === b.element + case "AnimalShapeSize": return b.tag === "AnimalShapeSize" && a.animal_shape_size === b.animal_shape_size + case "Patron": return b.tag === "Patron" && a.patron === b.patron + case "Ammunition": return b.tag === "Ammunition" && a.ammunition === b.ammunition + case "Animal": return b.tag === "Animal" && a.animal === b.animal + case "AnimalCare": return b.tag === "AnimalCare" && a.animal_care === b.animal_care + case "Armor": return b.tag === "Armor" && a.armor === b.armor + case "BandageOrRemedy": return b.tag === "BandageOrRemedy" && a.bandage_or_remedy === b.bandage_or_remedy + case "Book": return b.tag === "Book" && a.book === b.book + case "CeremonialItem": return b.tag === "CeremonialItem" && a.ceremonial_item === b.ceremonial_item + case "Clothes": return b.tag === "Clothes" && a.clothes === b.clothes + case "Container": return b.tag === "Container" && a.container === b.container + case "Elixir": return b.tag === "Elixir" && a.elixir === b.elixir + case "EquipmentOfBlessedOnes": return b.tag === "EquipmentOfBlessedOnes" && a.equipment_of_blessed_ones === b.equipment_of_blessed_ones + case "GemOrPreciousStone": return b.tag === "GemOrPreciousStone" && a.gem_or_precious_stone === b.gem_or_precious_stone + case "IlluminationLightSource": return b.tag === "IlluminationLightSource" && a.illumination_light_source === b.illumination_light_source + case "IlluminationRefillsOrSupplies": return b.tag === "IlluminationRefillsOrSupplies" && a.illumination_refills_or_supplies === b.illumination_refills_or_supplies + case "Jewelry": return b.tag === "Jewelry" && a.jewelry === b.jewelry + case "Liebesspielzeug": return b.tag === "Liebesspielzeug" && a.liebesspielzeug === b.liebesspielzeug + case "LuxuryGood": return b.tag === "LuxuryGood" && a.luxury_good === b.luxury_good + case "MagicalArtifact": return b.tag === "MagicalArtifact" && a.magical_artifact === b.magical_artifact + case "MusicalInstrument": return b.tag === "MusicalInstrument" && a.musical_instrument === b.musical_instrument + case "OrienteeringAid": return b.tag === "OrienteeringAid" && a.orienteering_aid === b.orienteering_aid + case "Poison": return b.tag === "Poison" && a.poison === b.poison + case "RopeOrChain": return b.tag === "RopeOrChain" && a.rope_or_chain === b.rope_or_chain + case "Stationary": return b.tag === "Stationary" && a.stationary === b.stationary + case "ThievesTool": return b.tag === "ThievesTool" && a.thieves_tool === b.thieves_tool + case "ToolOfTheTrade": return b.tag === "ToolOfTheTrade" && a.tool_of_the_trade === b.tool_of_the_trade + case "TravelGearOrTool": return b.tag === "TravelGearOrTool" && a.travel_gear_or_tool === b.travel_gear_or_tool + case "Vehicle": return b.tag === "Vehicle" && a.vehicle === b.vehicle + case "Weapon": return b.tag === "Weapon" && a.weapon === b.weapon + case "WeaponAccessory": return b.tag === "WeaponAccessory" && a.weapon_accessory === b.weapon_accessory + case "Reach": return b.tag === "Reach" && a.reach === b.reach + case "PatronCategory": return b.tag === "PatronCategory" && a.patron_category === b.patron_category + case "PersonalityTrait": return b.tag === "PersonalityTrait" && a.personality_trait === b.personality_trait + case "HairColor": return b.tag === "HairColor" && a.hair_color === b.hair_color + case "EyeColor": return b.tag === "EyeColor" && a.eye_color === b.eye_color + case "PactCategory": return b.tag === "PactCategory" && a.pact_category === b.pact_category + case "PactDomain": return b.tag === "PactDomain" && a.pact_domain === b.pact_domain + case "AnimistTribe": return b.tag === "AnimistTribe" && a.animist_tribe === b.animist_tribe + case "Influence": return b.tag === "Influence" && a.influence === b.influence + case "Condition": return b.tag === "Condition" && a.condition === b.condition + case "State": return b.tag === "State" && a.state === b.state + case "Disease": return b.tag === "Disease" && a.disease === b.disease + case "SexPractice": return b.tag === "SexPractice" && a.sex_practice === b.sex_practice + case "TradeSecret": return b.tag === "TradeSecret" && a.trade_secret === b.trade_secret + case "AnimalShape": return b.tag === "AnimalShape" && a.animal_shape === b.animal_shape + case "ArcaneBardTradition": return b.tag === "ArcaneBardTradition" && a.arcane_bard_tradition === b.arcane_bard_tradition + case "ArcaneDancerTradition": return b.tag === "ArcaneDancerTradition" && a.arcane_dancer_tradition === b.arcane_dancer_tradition + default: return assertExhaustive(a) + } +} + +/** + * Converts an identifier object to a string. + */ +export const identifierObjectToString = ( + obj: TagPropertyOptions[keyof TagPropertyOptions], +): string => { + // prettier-ignore + switch (obj.tag) { + case "Publication": return `${obj.tag}_${obj.publication}` + case "ExperienceLevel": return `${obj.tag}_${obj.experience_level}` + case "CoreRule": return `${obj.tag}_${obj.core_rule}` + case "FocusRule": return `${obj.tag}_${obj.focus_rule}` + case "Subject": return `${obj.tag}_${obj.subject}` + case "OptionalRule": return `${obj.tag}_${obj.optional_rule}` + case "Race": return `${obj.tag}_${obj.race}` + case "Culture": return `${obj.tag}_${obj.culture}` + case "Profession": return `${obj.tag}_${obj.profession}` + case "ProfessionVariant": return `${obj.tag}_${obj.profession_variant}` + case "Curriculum": return `${obj.tag}_${obj.curriculum}` + case "Guideline": return `${obj.tag}_${obj.guideline}` + case "Advantage": return `${obj.tag}_${obj.advantage}` + case "Disadvantage": return `${obj.tag}_${obj.disadvantage}` + case "GeneralSpecialAbility": return `${obj.tag}_${obj.general_special_ability}` + case "FatePointSpecialAbility": return `${obj.tag}_${obj.fate_point_special_ability}` + case "CombatSpecialAbility": return `${obj.tag}_${obj.combat_special_ability}` + case "MagicalSpecialAbility": return `${obj.tag}_${obj.magical_special_ability}` + case "StaffEnchantment": return `${obj.tag}_${obj.staff_enchantment}` + case "FamiliarSpecialAbility": return `${obj.tag}_${obj.familiar_special_ability}` + case "KarmaSpecialAbility": return `${obj.tag}_${obj.karma_special_ability}` + case "ProtectiveWardingCircleSpecialAbility": return `${obj.tag}_${obj.protective_warding_circle_special_ability}` + case "CombatStyleSpecialAbility": return `${obj.tag}_${obj.combat_style_special_ability}` + case "AdvancedCombatSpecialAbility": return `${obj.tag}_${obj.advanced_combat_special_ability}` + case "CommandSpecialAbility": return `${obj.tag}_${obj.command_special_ability}` + case "MagicStyleSpecialAbility": return `${obj.tag}_${obj.magic_style_special_ability}` + case "AdvancedMagicalSpecialAbility": return `${obj.tag}_${obj.advanced_magical_special_ability}` + case "SpellSwordEnchantment": return `${obj.tag}_${obj.spell_sword_enchantment}` + case "DaggerRitual": return `${obj.tag}_${obj.dagger_ritual}` + case "InstrumentEnchantment": return `${obj.tag}_${obj.instrument_enchantment}` + case "AttireEnchantment": return `${obj.tag}_${obj.attire_enchantment}` + case "OrbEnchantment": return `${obj.tag}_${obj.orb_enchantment}` + case "WandEnchantment": return `${obj.tag}_${obj.wand_enchantment}` + case "BrawlingSpecialAbility": return `${obj.tag}_${obj.brawling_special_ability}` + case "AncestorGlyph": return `${obj.tag}_${obj.ancestor_glyph}` + case "CeremonialItemSpecialAbility": return `${obj.tag}_${obj.ceremonial_item_special_ability}` + case "Sermon": return `${obj.tag}_${obj.sermon}` + case "LiturgicalStyleSpecialAbility": return `${obj.tag}_${obj.liturgical_style_special_ability}` + case "AdvancedKarmaSpecialAbility": return `${obj.tag}_${obj.advanced_karma_special_ability}` + case "Vision": return `${obj.tag}_${obj.vision}` + case "MagicalTradition": return `${obj.tag}_${obj.magical_tradition}` + case "BlessedTradition": return `${obj.tag}_${obj.blessed_tradition}` + case "PactGift": return `${obj.tag}_${obj.pact_gift}` + case "VampiricGift": return `${obj.tag}_${obj.vampiric_gift}` + case "SikaryanDrainSpecialAbility": return `${obj.tag}_${obj.sikaryan_drain_special_ability}` + case "LycantropicGift": return `${obj.tag}_${obj.lycantropic_gift}` + case "SkillStyleSpecialAbility": return `${obj.tag}_${obj.skill_style_special_ability}` + case "AdvancedSkillSpecialAbility": return `${obj.tag}_${obj.advanced_skill_special_ability}` + case "ArcaneOrbEnchantment": return `${obj.tag}_${obj.arcane_orb_enchantment}` + case "CauldronEnchantment": return `${obj.tag}_${obj.cauldron_enchantment}` + case "FoolsHatEnchantment": return `${obj.tag}_${obj.fools_hat_enchantment}` + case "ToyEnchantment": return `${obj.tag}_${obj.toy_enchantment}` + case "BowlEnchantment": return `${obj.tag}_${obj.bowl_enchantment}` + case "FatePointSexSpecialAbility": return `${obj.tag}_${obj.fate_point_sex_special_ability}` + case "SexSpecialAbility": return `${obj.tag}_${obj.sex_special_ability}` + case "WeaponEnchantment": return `${obj.tag}_${obj.weapon_enchantment}` + case "SickleRitual": return `${obj.tag}_${obj.sickle_ritual}` + case "RingEnchantment": return `${obj.tag}_${obj.ring_enchantment}` + case "ChronicleEnchantment": return `${obj.tag}_${obj.chronicle_enchantment}` + case "Krallenkettenzauber": return `${obj.tag}_${obj.krallenkettenzauber}` + case "Trinkhornzauber": return `${obj.tag}_${obj.trinkhornzauber}` + case "MagicalRune": return `${obj.tag}_${obj.magical_rune}` + case "MagicalSign": return `${obj.tag}_${obj.magical_sign}` + case "Language": return `${obj.tag}_${obj.language}` + case "Script": return `${obj.tag}_${obj.script}` + case "Continent": return `${obj.tag}_${obj.continent}` + case "SocialStatus": return `${obj.tag}_${obj.social_status}` + case "Attribute": return `${obj.tag}_${obj.attribute}` + case "Skill": return `${obj.tag}_${obj.skill}` + case "SkillGroup": return `${obj.tag}_${obj.skill_group}` + case "CloseCombatTechnique": return `${obj.tag}_${obj.close_combat_technique}` + case "RangedCombatTechnique": return `${obj.tag}_${obj.ranged_combat_technique}` + case "Spell": return `${obj.tag}_${obj.spell}` + case "Ritual": return `${obj.tag}_${obj.ritual}` + case "Cantrip": return `${obj.tag}_${obj.cantrip}` + case "Property": return `${obj.tag}_${obj.property}` + case "LiturgicalChant": return `${obj.tag}_${obj.liturgical_chant}` + case "Ceremony": return `${obj.tag}_${obj.ceremony}` + case "Blessing": return `${obj.tag}_${obj.blessing}` + case "Aspect": return `${obj.tag}_${obj.aspect}` + case "Curse": return `${obj.tag}_${obj.curse}` + case "ElvenMagicalSong": return `${obj.tag}_${obj.elven_magical_song}` + case "DominationRitual": return `${obj.tag}_${obj.domination_ritual}` + case "MagicalMelody": return `${obj.tag}_${obj.magical_melody}` + case "MagicalDance": return `${obj.tag}_${obj.magical_dance}` + case "JesterTrick": return `${obj.tag}_${obj.jester_trick}` + case "AnimistPower": return `${obj.tag}_${obj.animist_power}` + case "GeodeRitual": return `${obj.tag}_${obj.geode_ritual}` + case "ZibiljaRitual": return `${obj.tag}_${obj.zibilja_ritual}` + case "AnimalType": return `${obj.tag}_${obj.animal_type}` + case "TargetCategory": return `${obj.tag}_${obj.target_category}` + case "General": return `${obj.tag}_${obj.general}` + case "Element": return `${obj.tag}_${obj.element}` + case "AnimalShapeSize": return `${obj.tag}_${obj.animal_shape_size}` + case "Patron": return `${obj.tag}_${obj.patron}` + case "Ammunition": return `${obj.tag}_${obj.ammunition}` + case "Animal": return `${obj.tag}_${obj.animal}` + case "AnimalCare": return `${obj.tag}_${obj.animal_care}` + case "Armor": return `${obj.tag}_${obj.armor}` + case "BandageOrRemedy": return `${obj.tag}_${obj.bandage_or_remedy}` + case "Book": return `${obj.tag}_${obj.book}` + case "CeremonialItem": return `${obj.tag}_${obj.ceremonial_item}` + case "Clothes": return `${obj.tag}_${obj.clothes}` + case "Container": return `${obj.tag}_${obj.container}` + case "Elixir": return `${obj.tag}_${obj.elixir}` + case "EquipmentOfBlessedOnes": return `${obj.tag}_${obj.equipment_of_blessed_ones}` + case "GemOrPreciousStone": return `${obj.tag}_${obj.gem_or_precious_stone}` + case "IlluminationLightSource": return `${obj.tag}_${obj.illumination_light_source}` + case "IlluminationRefillsOrSupplies": return `${obj.tag}_${obj.illumination_refills_or_supplies}` + case "Jewelry": return `${obj.tag}_${obj.jewelry}` + case "Liebesspielzeug": return `${obj.tag}_${obj.liebesspielzeug}` + case "LuxuryGood": return `${obj.tag}_${obj.luxury_good}` + case "MagicalArtifact": return `${obj.tag}_${obj.magical_artifact}` + case "MusicalInstrument": return `${obj.tag}_${obj.musical_instrument}` + case "OrienteeringAid": return `${obj.tag}_${obj.orienteering_aid}` + case "Poison": return `${obj.tag}_${obj.poison}` + case "RopeOrChain": return `${obj.tag}_${obj.rope_or_chain}` + case "Stationary": return `${obj.tag}_${obj.stationary}` + case "ThievesTool": return `${obj.tag}_${obj.thieves_tool}` + case "ToolOfTheTrade": return `${obj.tag}_${obj.tool_of_the_trade}` + case "TravelGearOrTool": return `${obj.tag}_${obj.travel_gear_or_tool}` + case "Vehicle": return `${obj.tag}_${obj.vehicle}` + case "Weapon": return `${obj.tag}_${obj.weapon}` + case "WeaponAccessory": return `${obj.tag}_${obj.weapon_accessory}` + case "Reach": return `${obj.tag}_${obj.reach}` + case "PatronCategory": return `${obj.tag}_${obj.patron_category}` + case "PersonalityTrait": return `${obj.tag}_${obj.personality_trait}` + case "HairColor": return `${obj.tag}_${obj.hair_color}` + case "EyeColor": return `${obj.tag}_${obj.eye_color}` + case "PactCategory": return `${obj.tag}_${obj.pact_category}` + case "PactDomain": return `${obj.tag}_${obj.pact_domain}` + case "AnimistTribe": return `${obj.tag}_${obj.animist_tribe}` + case "Influence": return `${obj.tag}_${obj.influence}` + case "Condition": return `${obj.tag}_${obj.condition}` + case "State": return `${obj.tag}_${obj.state}` + case "Disease": return `${obj.tag}_${obj.disease}` + case "SexPractice": return `${obj.tag}_${obj.sex_practice}` + case "TradeSecret": return `${obj.tag}_${obj.trade_secret}` + case "AnimalShape": return `${obj.tag}_${obj.animal_shape}` + case "ArcaneBardTradition": return `${obj.tag}_${obj.arcane_bard_tradition}` + case "ArcaneDancerTradition": return `${obj.tag}_${obj.arcane_dancer_tradition}` + default: return assertExhaustive(obj) + } +} diff --git a/src/shared/domain/pact.ts b/src/shared/domain/pact.ts index 5604e47d..546464e4 100644 --- a/src/shared/domain/pact.ts +++ b/src/shared/domain/pact.ts @@ -1,3 +1,9 @@ +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" +import { + PactCategoryReference, + PactDomainReference, +} from "optolith-database-schema/types/_SimpleReferences" + /** * A active pact between a character and a creature. */ @@ -44,3 +50,27 @@ export type PactDomain = kind: "Custom" name: string } + +/** + * A dependency on a pact. + */ +export type PactDependency = { + sourceId: ActivatableIdentifier + index: number + isPartOfDisjunction: boolean + + /** + * The required pact category. + */ + category: PactCategoryReference + + /** + * The required domain(s). + */ + domain?: PactDomainReference[] + + /** + * The required pact level. + */ + level?: number +} diff --git a/src/shared/domain/race.ts b/src/shared/domain/race.ts index 304c0f21..3159787d 100644 --- a/src/shared/domain/race.ts +++ b/src/shared/domain/race.ts @@ -1,4 +1,5 @@ import { Race, RaceVariant } from "optolith-database-schema/types/Race" +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" import { mapNullableDefault } from "../utils/nullable.ts" import { TranslateMap } from "../utils/translate.ts" @@ -26,3 +27,34 @@ export const getFullRaceName = ( str => ` (${str})`, "", )}` + +/** + * A dependency on a race. + */ +export type RaceDependency = Readonly<{ + /** + * The identifier of the dependency source. + */ + sourceId: ActivatableIdentifier + + /** + * The top-level index of the prerequisite. If the prerequisite is part of a + * group or disjunction, this is the index of the group or disjunction. + */ + index: number + + /** + * Is the source prerequisite part of a prerequisite disjunction? + */ + isPartOfDisjunction: boolean + + /** + * The required race's identifier. + */ + id: number + + /** + * Whether the required race is required to be active or inactive. + */ + active: boolean +}> diff --git a/src/shared/domain/rated/animistPower.ts b/src/shared/domain/rated/animistPower.ts new file mode 100644 index 00000000..28916bf5 --- /dev/null +++ b/src/shared/domain/rated/animistPower.ts @@ -0,0 +1,27 @@ +import { AnimistPowerImprovementCost } from "optolith-database-schema/types/magicalActions/AnimistPower" +import { mapNullable } from "../../utils/nullable.ts" +import { assertExhaustive } from "../../utils/typeSafety.ts" +import { Activatable, getFirstOptionOfType } from "../activatable/activatableEntry.ts" +import { ImprovementCost, fromRaw } from "../adventurePoints/improvementCost.ts" +import { GetById } from "../getTypes.ts" + +/** + * Returns the improvement cost for an animist power. + */ +export const getImprovementCostForAnimistPower = ( + getStaticPatronById: GetById.Static.Patron, + dynamicTradition: Activatable, + improvementCost: AnimistPowerImprovementCost, +): ImprovementCost | undefined => { + switch (improvementCost.tag) { + case "Fixed": + return fromRaw(improvementCost.fixed) + case "ByPrimaryPatron": { + const primaryPatronId = getFirstOptionOfType(dynamicTradition, "Patron") + const primaryPatron = mapNullable(primaryPatronId, getStaticPatronById) + return mapNullable(primaryPatron?.improvement_cost, fromRaw) + } + default: + return assertExhaustive(improvementCost) + } +} diff --git a/src/shared/domain/rated/attribute.ts b/src/shared/domain/rated/attribute.ts index 619f7c59..7d30ff0a 100644 --- a/src/shared/domain/rated/attribute.ts +++ b/src/shared/domain/rated/attribute.ts @@ -1,4 +1,5 @@ import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences" +import { ImprovementCost } from "../adventurePoints/improvementCost.ts" import { Rated } from "./ratedEntry.ts" /** @@ -6,6 +7,11 @@ import { Rated } from "./ratedEntry.ts" */ export const minimumAttributeValue = 8 +/** + * The improvement cost of an attribute. + */ +export const attributeImprovementCost = ImprovementCost.E + /** * Creates an initial dynamic attribute entry. */ diff --git a/src/shared/domain/rated/attributeBounds.ts b/src/shared/domain/rated/attributeBounds.ts index 8061fbbc..7a2c7640 100644 --- a/src/shared/domain/rated/attributeBounds.ts +++ b/src/shared/domain/rated/attributeBounds.ts @@ -9,6 +9,10 @@ import { AttributeIdentifier } from "../identifier.ts" import { getAttributeValue } from "./attribute.ts" import { getHighestRequiredAttributeForCombatTechnique } from "./combatTechnique.ts" import { getHighestRequiredAttributeForLiturgicalChant } from "./liturgicalChant.ts" +import { + PrimaryAttributeDependency, + primaryAttributeDependencyToRatedDependency, +} from "./primaryAttribute.ts" import { RatedDependency, flattenMinimumRestrictions } from "./ratedDependency.ts" import { Rated } from "./ratedEntry.ts" import { getHighestRequiredAttributeForSkill } from "./skill.ts" @@ -23,9 +27,9 @@ export const getAttributeMinimum = ( purchasedArcaneEnergy: number, purchasedKarmaPoints: number, singleHighestMagicalPrimaryAttributeId: number | undefined, - magicalPrimaryAttributeDependencies: RatedDependency[], + magicalPrimaryAttributeDependencies: PrimaryAttributeDependency[], blessedPrimaryAttributeId: number | undefined, - blessedPrimaryAttributeDependencies: RatedDependency[], + blessedPrimaryAttributeDependencies: PrimaryAttributeDependency[], filterApplyingDependencies: (dependencies: RatedDependency[]) => RatedDependency[], getSkillCheckAttributeMinimum: (id: number) => number | undefined, dynamicAttribute: Rated, @@ -43,7 +47,9 @@ export const getAttributeMinimum = ( ? [ purchasedArcaneEnergy, ...flattenMinimumRestrictions( - filterApplyingDependencies(magicalPrimaryAttributeDependencies), + filterApplyingDependencies( + magicalPrimaryAttributeDependencies.map(primaryAttributeDependencyToRatedDependency), + ), ), ] : []), @@ -51,7 +57,9 @@ export const getAttributeMinimum = ( ? [ purchasedKarmaPoints, ...flattenMinimumRestrictions( - filterApplyingDependencies(blessedPrimaryAttributeDependencies), + filterApplyingDependencies( + blessedPrimaryAttributeDependencies.map(primaryAttributeDependencyToRatedDependency), + ), ), ] : []), diff --git a/src/shared/domain/rated/combatTechnique.ts b/src/shared/domain/rated/combatTechnique.ts index 7fef454b..fe8e2bb0 100644 --- a/src/shared/domain/rated/combatTechnique.ts +++ b/src/shared/domain/rated/combatTechnique.ts @@ -1,11 +1,8 @@ import { CloseCombatTechnique } from "optolith-database-schema/types/CombatTechnique_Close" 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 { - Activatable, - PredefinedActivatableOption, - countOptions, -} from "../activatable/activatableEntry.ts" +import { Activatable, countOptions } from "../activatable/activatableEntry.ts" import { AttributeIdentifier } from "../identifier.ts" import { getAttributeValue } from "./attribute.ts" import { Rated } from "./ratedEntry.ts" @@ -48,10 +45,16 @@ export type CombinedCombatTechnique = */ export const getCombinedId = ( combatTechnique: CombinedCombatTechnique, -): PredefinedActivatableOption["id"] => +): CombatTechniqueIdentifier => combatTechnique.tag === "CloseCombatTechnique" - ? { type: "CloseCombatTechnique", value: combatTechnique.closeCombatTechnique.id } - : { type: "RangedCombatTechnique", value: combatTechnique.rangedCombatTechnique.id } + ? { + tag: "CloseCombatTechnique", + close_combat_technique: combatTechnique.closeCombatTechnique.id, + } + : { + tag: "RangedCombatTechnique", + ranged_combat_technique: combatTechnique.rangedCombatTechnique.id, + } /** * Returns the primary attribute of a combined combat technique. diff --git a/src/shared/domain/rated/combatTechniqueBounds.ts b/src/shared/domain/rated/combatTechniqueBounds.ts index f944bb3b..e7f9b42d 100644 --- a/src/shared/domain/rated/combatTechniqueBounds.ts +++ b/src/shared/domain/rated/combatTechniqueBounds.ts @@ -2,12 +2,13 @@ import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel" import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences" import { filterNonNullable } from "../../utils/array.ts" import { Activatable, countOptions, isActive } from "../activatable/activatableEntry.ts" +import { FilterApplyingRatedDependencies } from "../dependencies/filterApplyingDependencies.ts" import { CombinedCombatTechnique, getCombinedId, getCombinedPrimaryAttribute, } from "./combatTechnique.ts" -import { RatedDependency, flattenMinimumRestrictions } from "./ratedDependency.ts" +import { flattenMinimumRestrictions } from "./ratedDependency.ts" import { Rated } from "./ratedEntry.ts" const getCombatTechniqueMinimumForHunter = ( @@ -31,7 +32,7 @@ export const getCombatTechniqueMinimum = ( staticCombatTechnique: CombinedCombatTechnique, dynamicCombatTechnique: Rated, hunter: Activatable | undefined, - filterApplyingDependencies: (dependencies: RatedDependency[]) => RatedDependency[], + filterApplyingDependencies: FilterApplyingRatedDependencies, ): number => { const minimumValues = filterNonNullable([ 6, diff --git a/src/shared/domain/rated/enhancement.ts b/src/shared/domain/rated/enhancement.ts index 26f5801d..1d4305b2 100644 --- a/src/shared/domain/rated/enhancement.ts +++ b/src/shared/domain/rated/enhancement.ts @@ -10,15 +10,26 @@ export type EnhancementDependency = /** * The depending enhancement. */ - id: number + sourceId: number } | { tag: "External" /** - * The depending activatable. + * The identifier of the dependency source. */ - id: ActivatableIdentifier + sourceId: ActivatableIdentifier + + /** + * The top-level index of the prerequisite. If the prerequisite is part of a + * group or disjunction, this is the index of the group or disjunction. + */ + index: number + + /** + * Is the source prerequisite part of a prerequisite disjunction? + */ + isPartOfDisjunction: boolean } /** diff --git a/src/shared/domain/rated/liturgicalChant.ts b/src/shared/domain/rated/liturgicalChant.ts index fb2aef0b..54347092 100644 --- a/src/shared/domain/rated/liturgicalChant.ts +++ b/src/shared/domain/rated/liturgicalChant.ts @@ -13,7 +13,7 @@ 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 { AspectIdentifier } from "../identifier.ts" +import { AspectIdentifier, createIdentifierObject } from "../identifier.ts" import { LiturgiesSortOrder } from "../sortOrders.ts" import { DisplayedActiveLiturgy } from "./liturgicalChantActive.ts" import { DisplayedInactiveLiturgy } from "./liturgicalChantInactive.ts" @@ -83,6 +83,21 @@ export const flattenAspectIds = (traditions: SkillTradition[]): number[] => }) .map(aspect => aspect.id.aspect) +/** + * Checks if the passed liturgical chant features the passed aspect. + */ +export const hasAspectById = (id: number, traditions: SkillTradition[]): boolean => + traditions.some(tradition => { + switch (tradition.tag) { + case "GeneralAspect": + return tradition.general_aspect.id.aspect === id + case "Tradition": + return tradition.tradition.aspects?.some(aspect => aspect.id.aspect === id) ?? false + default: + return assertExhaustive(tradition) + } + }) + /** * Returns the translation of all aspects relevant to the current active blessed * tradition. If the tradition has no aspects, the name of the tradition is used @@ -221,10 +236,10 @@ export const getHighestRequiredAttributeForLiturgicalChant = ( return undefined } - const exceptionalSkillBonus = countOptions(exceptionalSkill, { - type, - value: staticLiturgicalChant.id, - }) + const exceptionalSkillBonus = countOptions( + exceptionalSkill, + createIdentifierObject(type, staticLiturgicalChant.id), + ) return { id: singleHighestAttributeId, diff --git a/src/shared/domain/rated/liturgicalChantActive.ts b/src/shared/domain/rated/liturgicalChantActive.ts index 17ddeb1a..95d505d3 100644 --- a/src/shared/domain/rated/liturgicalChantActive.ts +++ b/src/shared/domain/rated/liturgicalChantActive.ts @@ -4,7 +4,7 @@ import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel" import { LiturgicalChant } from "optolith-database-schema/types/LiturgicalChant" import { isNotNullish } from "../../utils/nullable.ts" import { assertExhaustive } from "../../utils/typeSafety.ts" -import { Activatable } from "../activatable/activatableEntry.ts" +import { Activatable, TinyActivatable } from "../activatable/activatableEntry.ts" import { FilterApplyingRatedDependencies } from "../dependencies/filterApplyingDependencies.ts" import { getHighestAttributeValue } from "./attribute.ts" import { @@ -26,6 +26,7 @@ import { export type DisplayedActiveBlessing = { kind: "blessing" static: Blessing + dynamic: TinyActivatable } /** diff --git a/src/shared/domain/rated/liturgicalChantActiveBounds.ts b/src/shared/domain/rated/liturgicalChantActiveBounds.ts index b8a6aa7b..c3ee1816 100644 --- a/src/shared/domain/rated/liturgicalChantActiveBounds.ts +++ b/src/shared/domain/rated/liturgicalChantActiveBounds.ts @@ -4,8 +4,10 @@ import { AttributeReference } from "optolith-database-schema/types/_SimpleRefere import { SkillCheck } from "optolith-database-schema/types/_SkillCheck" import { filterNonNullable } from "../../utils/array.ts" import { Activatable, countOptions } from "../activatable/activatableEntry.ts" +import { FilterApplyingRatedDependencies } from "../dependencies/filterApplyingDependencies.ts" +import { createIdentifierObject } from "../identifier.ts" import { flattenAspectIds } from "./liturgicalChant.ts" -import { RatedDependency, flattenMinimumRestrictions } from "./ratedDependency.ts" +import { flattenMinimumRestrictions } from "./ratedDependency.ts" import { ActivatableRated, ActiveActivatableRated } from "./ratedEntry.ts" const getLiturgicalChantMinimumFromAspectKnowledgePrerequistes = ( @@ -32,7 +34,7 @@ export const getLiturgicalChantMinimum = ( activeAspectKnowledges: number[], staticLiturgicalChant: { traditions: SkillTradition[] }, dynamicLiturgicalChant: ActivatableRated, - filterApplyingDependencies: (dependencies: RatedDependency[]) => RatedDependency[], + filterApplyingDependencies: FilterApplyingRatedDependencies, ): number | undefined => { const minimumValues: number[] = filterNonNullable([ ...flattenMinimumRestrictions(filterApplyingDependencies(dynamicLiturgicalChant.dependencies)), @@ -74,10 +76,10 @@ export const getLiturgicalChantMaximum = ( ), ]) - const exceptionalSkillBonus = countOptions(exceptionalSkill, { - type, - value: staticLiturgicalChant.id, - }) + const exceptionalSkillBonus = countOptions( + exceptionalSkill, + createIdentifierObject(type, staticLiturgicalChant.id), + ) return Math.min(...maximumValues) + exceptionalSkillBonus } diff --git a/src/shared/domain/rated/magicalActions.ts b/src/shared/domain/rated/magicalActions.ts index e4e6ebd7..1db5d59c 100644 --- a/src/shared/domain/rated/magicalActions.ts +++ b/src/shared/domain/rated/magicalActions.ts @@ -1,4 +1,5 @@ import { ImprovementCost } from "../adventurePoints/improvementCost.ts" +import { ActivatableRated } from "./ratedEntry.ts" /** * The improvement cost for a curse is always the same. @@ -14,3 +15,17 @@ export const dominationRitualsImprovementCost = ImprovementCost.B * The improvement cost for a curse is always the same. */ export const geodeRitualsImprovementCost = ImprovementCost.B + +/** + * Creates an initial dynamic magical action entry. + */ +export const createEmptyDynamicMagicalAction = (id: number): ActivatableRated => ({ + id, + value: undefined, + cachedAdventurePoints: { + general: 0, + bound: 0, + }, + dependencies: [], + boundAdventurePoints: [], +}) diff --git a/src/shared/domain/rated/magicalRune.ts b/src/shared/domain/rated/magicalRune.ts new file mode 100644 index 00000000..81819971 --- /dev/null +++ b/src/shared/domain/rated/magicalRune.ts @@ -0,0 +1,21 @@ +import { MagicalRuneImprovementCost } from "optolith-database-schema/types/magicalActions/MagicalRune" +import { assertExhaustive } from "../../utils/typeSafety.ts" +import { ImprovementCost, fromRaw } from "../adventurePoints/improvementCost.ts" + +/** + * Returns the improvement cost for a magical rune. + */ +export const getImprovementCostForMagicalRune = ( + improvementCost: MagicalRuneImprovementCost, +): ImprovementCost | undefined => { + switch (improvementCost.tag) { + case "Constant": + return fromRaw(improvementCost.constant.value) + case "DerivedFromOption": { + // TODO + return undefined + } + default: + return assertExhaustive(improvementCost) + } +} diff --git a/src/shared/domain/rated/primaryAttribute.ts b/src/shared/domain/rated/primaryAttribute.ts index 6e7a1a05..17072463 100644 --- a/src/shared/domain/rated/primaryAttribute.ts +++ b/src/shared/domain/rated/primaryAttribute.ts @@ -1,7 +1,9 @@ import { Attribute } from "optolith-database-schema/types/Attribute" +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" import { CombinedActiveBlessedTradition } from "../activatable/blessedTradition.ts" import { CombinedActiveMagicalTradition } from "../activatable/magicalTradition.ts" import { createEmptyDynamicAttribute } from "./attribute.ts" +import { RatedDependency, ValueRestriction } from "./ratedDependency.ts" import { Rated } from "./ratedEntry.ts" /** @@ -116,3 +118,41 @@ export const getBlessedPrimaryAttribute = ( } } } + +/** + * Describes a dependency on a certain rated entry. + */ +export type PrimaryAttributeDependency = Readonly<{ + /** + * The source of the dependency. + */ + sourceId: ActivatableIdentifier + + /** + * The top-level index of the prerequisite. If the prerequisite is part of a + * group or disjunction, this is the index of the group or disjunction. + */ + index: number + + /** + * Is the source prerequisite is part of a prerequisite disjunction? + */ + isPartOfDisjunction: boolean + + /** + * The required value. + */ + value: ValueRestriction +}> + +/** + * Converts a primary attribute dependency to a rated dependency. + */ +export const primaryAttributeDependencyToRatedDependency = ( + dep: PrimaryAttributeDependency, +): RatedDependency => ({ + source: dep.sourceId, + index: dep.index, + isPartOfDisjunction: dep.isPartOfDisjunction, + value: { tag: "Fixed", value: dep.value }, +}) diff --git a/src/shared/domain/rated/ratedDependency.ts b/src/shared/domain/rated/ratedDependency.ts index c1b23149..48bdbf54 100644 --- a/src/shared/domain/rated/ratedDependency.ts +++ b/src/shared/domain/rated/ratedDependency.ts @@ -1,7 +1,10 @@ +import { SkillIdentifier } from "optolith-database-schema/types/_Identifier" import { ActivatableIdentifier, SkillWithEnhancementsIdentifier, } from "optolith-database-schema/types/_IdentifierGroup" +import { RatedMinimumNumberPrerequisiteTarget } from "optolith-database-schema/types/prerequisites/single/RatedMinimumNumberPrerequisite" +import { assertExhaustive } from "../../utils/typeSafety.ts" /** * A required value from a prerequisite. Can either require a minimum or a @@ -52,40 +55,80 @@ export const compareWithRestriction = ( ? true : value <= restriction.maximum +type FixedValue = { + tag: "Fixed" + value: ValueRestriction +} + +type MinimumNumberAtMinimumValue = { + tag: "MinimumNumberAtMinimumValue" + minimumCount: number + minimumValue: number + target: RatedMinimumNumberPrerequisiteTarget +} + +type Sum = { + tag: "Sum" + sum: number + targetIds: SkillIdentifier[] +} + /** * Describes a dependency on a certain rated entry. */ -export type RatedDependency = { +export type RatedDependency = Readonly<{ /** - * The source of the dependency. + * The identifier of the dependency source. */ - readonly source: ActivatableIdentifier | SkillWithEnhancementsIdentifier + source: ActivatableIdentifier | SkillWithEnhancementsIdentifier /** * The top-level index of the prerequisite. If the prerequisite is part of a * group or disjunction, this is the index of the group or disjunction. */ - readonly index: number + index: number /** - * Is the source prerequisite is part of a prerequisite disjunction? + * Is the source prerequisite part of a prerequisite disjunction? */ - readonly isPartOfDisjunction: boolean + isPartOfDisjunction: boolean /** * The required value. */ - readonly value: ValueRestriction -} + value: FixedValue | MinimumNumberAtMinimumValue | Sum +}> /** * Flattens the minimum restrictions of a list of dependencies. */ export const flattenMinimumRestrictions = (dependencies: RatedDependency[]): number[] => - dependencies.flatMap(dep => (isMinimumRestriction(dep.value) ? [dep.value.minimum] : [])) + dependencies.flatMap(dep => { + switch (dep.value.tag) { + case "Fixed": + return isMinimumRestriction(dep.value.value) ? [dep.value.value.minimum] : [] + case "MinimumNumberAtMinimumValue": + return [dep.value.minimumValue] + case "Sum": + // TODO: Implement + return [] + default: + return assertExhaustive(dep.value) + } + }) /** * Flattens the maximum restrictions of a list of dependencies. */ export const flattenMaximumRestrictions = (dependencies: RatedDependency[]): number[] => - dependencies.flatMap(dep => (isMaximumRestriction(dep.value) ? [dep.value.maximum] : [])) + dependencies.flatMap(dep => { + switch (dep.value.tag) { + case "Fixed": + return isMaximumRestriction(dep.value.value) ? [dep.value.value.maximum] : [] + case "MinimumNumberAtMinimumValue": + case "Sum": + return [] + default: + return assertExhaustive(dep.value) + } + }) diff --git a/src/shared/domain/rated/ratedEntry.ts b/src/shared/domain/rated/ratedEntry.ts index 620808d2..f4ce94b3 100644 --- a/src/shared/domain/rated/ratedEntry.ts +++ b/src/shared/domain/rated/ratedEntry.ts @@ -1,7 +1,7 @@ +import { AdventurePointsCache, emptyAdventurePointsCache } from "../adventurePoints/cache.ts" import { ImprovementCost } from "../adventurePoints/improvementCost.ts" import { - BoundAdventurePoints, - RatedAdventurePointsCache, + BoundAdventurePointsForRated, cachedAdventurePoints, cachedAdventurePointsForActivatable, } from "../adventurePoints/ratedEntry.ts" @@ -30,7 +30,7 @@ export type Rated = { /** * The accumulated used adventure points value of all value increases. */ - readonly cachedAdventurePoints: RatedAdventurePointsCache + readonly cachedAdventurePoints: AdventurePointsCache /** * The list of dependencies. @@ -39,11 +39,11 @@ export type Rated = { /** * A list of bound adventure points. Bound adventure points are granted by the - * GM and can only be spent on the entry. They don’t effect the costs of the + * GM and can only be spent on the entry. They don’t affect the costs of the * rating at the time of granting, so the rating at which they have been * granted is stored as well. */ - readonly boundAdventurePoints: BoundAdventurePoints[] + readonly boundAdventurePoints: BoundAdventurePointsForRated[] } /** @@ -67,7 +67,7 @@ export type RatedHelpers = { value?: RatedValue, options?: Partial<{ dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] + boundAdventurePoints: BoundAdventurePointsForRated[] }>, ) => Rated @@ -111,10 +111,7 @@ export const createRatedHelpers = (config: { updateCachedAdventurePoints({ id, value: Math.max(minValue, value), - cachedAdventurePoints: { - general: 0, - bound: 0, - }, + cachedAdventurePoints: emptyAdventurePointsCache, dependencies, boundAdventurePoints, }) @@ -156,7 +153,7 @@ export type ActivatableRated = { /** * The accumulated used adventure points value of all value increases. */ - readonly cachedAdventurePoints: RatedAdventurePointsCache + readonly cachedAdventurePoints: AdventurePointsCache /** * The list of dependencies. @@ -169,7 +166,7 @@ export type ActivatableRated = { * rating at the time of granting, so the rating at which they have been * granted is stored as well. */ - readonly boundAdventurePoints: BoundAdventurePoints[] + readonly boundAdventurePoints: BoundAdventurePointsForRated[] } /** @@ -217,7 +214,7 @@ export type ActivatableRatedHelpers = { value?: ActivatableRatedValue, options?: Partial<{ dependencies: RatedDependency[] - boundAdventurePoints: BoundAdventurePoints[] + boundAdventurePoints: BoundAdventurePointsForRated[] }>, ) => ActivatableRated @@ -263,10 +260,7 @@ export const createActivatableRatedHelpers = (config: { updateCachedAdventurePoints({ id, value: value === undefined ? undefined : Math.max(minValue, value), - cachedAdventurePoints: { - general: 0, - bound: 0, - }, + cachedAdventurePoints: emptyAdventurePointsCache, dependencies, boundAdventurePoints, }) @@ -308,7 +302,7 @@ export type ActivatableRatedWithEnhancements = { * The accumulated used adventure points value of all value increases and * enhancements. */ - readonly cachedAdventurePoints: RatedAdventurePointsCache + readonly cachedAdventurePoints: AdventurePointsCache /** * The list of dependencies. @@ -321,7 +315,7 @@ export type ActivatableRatedWithEnhancements = { * rating at the time of granting, so the rating at which they have been * granted is stored as well. */ - readonly boundAdventurePoints: BoundAdventurePoints[] + readonly boundAdventurePoints: BoundAdventurePointsForRated[] /** * The currently active enhancements for that entry. diff --git a/src/shared/domain/rated/skill.ts b/src/shared/domain/rated/skill.ts index a8ed06a0..3cd28184 100644 --- a/src/shared/domain/rated/skill.ts +++ b/src/shared/domain/rated/skill.ts @@ -45,8 +45,8 @@ export const getHighestRequiredAttributeForSkill = ( } const exceptionalSkillBonus = countOptions(exceptionalSkill, { - type: "Skill", - value: staticSkill.id, + tag: "Skill", + skill: staticSkill.id, }) return { diff --git a/src/shared/domain/rated/skillBounds.ts b/src/shared/domain/rated/skillBounds.ts index 6ae456bb..3f3f563f 100644 --- a/src/shared/domain/rated/skillBounds.ts +++ b/src/shared/domain/rated/skillBounds.ts @@ -3,8 +3,9 @@ import { Skill } from "optolith-database-schema/types/Skill" import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences" import { filterNonNullable } from "../../utils/array.ts" import { Activatable, countOptions, isActive } from "../activatable/activatableEntry.ts" +import { FilterApplyingRatedDependencies } from "../dependencies/filterApplyingDependencies.ts" import { SkillIdentifier } from "../identifier.ts" -import { RatedDependency, flattenMinimumRestrictions } from "./ratedDependency.ts" +import { flattenMinimumRestrictions } from "./ratedDependency.ts" import { Rated } from "./ratedEntry.ts" import { getSkillValue } from "./skill.ts" @@ -38,7 +39,7 @@ export const getSkillMinimum = ( getDynamicSkill: (id: number) => Rated | undefined, dynamicSkill: Rated, craftInstruments: Activatable | undefined, - filterApplyingDependencies: (dependencies: RatedDependency[]) => RatedDependency[], + filterApplyingDependencies: FilterApplyingRatedDependencies, ): number => { const minimumValues = filterNonNullable([ 0, @@ -67,8 +68,8 @@ export const getSkillMaximum = ( ]) const exceptionalSkillBonus = countOptions(exceptionalSkill, { - type: "Skill", - value: staticSkill.id, + tag: "Skill", + skill: staticSkill.id, }) return Math.min(...maximumValues) + exceptionalSkillBonus diff --git a/src/shared/domain/rated/spell.ts b/src/shared/domain/rated/spell.ts index 759a86b1..22be9f22 100644 --- a/src/shared/domain/rated/spell.ts +++ b/src/shared/domain/rated/spell.ts @@ -24,7 +24,11 @@ import { fromRaw, } from "../adventurePoints/improvementCost.ts" import { All, GetById } from "../getTypes.ts" -import { MagicalTraditionIdentifier, createIdentifierObject } from "../identifier.ts" +import { + MagicalTraditionIdentifier, + createIdentifierObject, + getCreateIdentifierObject, +} from "../identifier.ts" import { SpellsSortOrder } from "../sortOrders.ts" import { cursesImprovementCost, @@ -113,6 +117,7 @@ export const getSpellworksAbove10ByProperty = ( dynamicAnimistPowers: ActivatableRatedMap, dynamicGeodeRituals: ActivatableRatedMap, dynamicZibiljaRituals: ActivatableRatedMap, + dynamicMagicalRunes: ActivatableRatedMap, ): Record => { const prepareEntity = ( ratedMap: ActivatableRatedMap, @@ -129,23 +134,18 @@ export const getSpellworksAbove10ByProperty = ( ) return [ - ...prepareEntity(dynamicSpells, id => ({ tag: "Spell", spell: id })), - ...prepareEntity(dynamicRituals, id => ({ tag: "Ritual", ritual: id })), - ...prepareEntity(dynamicCurses, id => ({ tag: "Curse", curse: id })), - ...prepareEntity(dynamicElvenMagicalSongs, id => ({ - tag: "ElvenMagicalSong", - elven_magical_song: id, - })), - ...prepareEntity(dynamicDominationRituals, id => ({ - tag: "DominationRitual", - domination_ritual: id, - })), - ...prepareEntity(dynamicMagicalDances, id => ({ tag: "MagicalDance", magical_dance: id })), - ...prepareEntity(dynamicMagicalMelodies, id => ({ tag: "MagicalMelody", magical_melody: id })), - ...prepareEntity(dynamicJesterTricks, id => ({ tag: "JesterTrick", jester_trick: id })), - ...prepareEntity(dynamicAnimistPowers, id => ({ tag: "AnimistPower", animist_power: id })), - ...prepareEntity(dynamicGeodeRituals, id => ({ tag: "GeodeRitual", geode_ritual: id })), - ...prepareEntity(dynamicZibiljaRituals, id => ({ tag: "ZibiljaRitual", zibilja_ritual: id })), + ...prepareEntity(dynamicSpells, getCreateIdentifierObject("Spell")), + ...prepareEntity(dynamicRituals, getCreateIdentifierObject("Ritual")), + ...prepareEntity(dynamicCurses, getCreateIdentifierObject("Curse")), + ...prepareEntity(dynamicElvenMagicalSongs, getCreateIdentifierObject("ElvenMagicalSong")), + ...prepareEntity(dynamicDominationRituals, getCreateIdentifierObject("DominationRitual")), + ...prepareEntity(dynamicMagicalDances, getCreateIdentifierObject("MagicalDance")), + ...prepareEntity(dynamicMagicalMelodies, getCreateIdentifierObject("MagicalMelody")), + ...prepareEntity(dynamicJesterTricks, getCreateIdentifierObject("JesterTrick")), + ...prepareEntity(dynamicAnimistPowers, getCreateIdentifierObject("AnimistPower")), + ...prepareEntity(dynamicGeodeRituals, getCreateIdentifierObject("GeodeRitual")), + ...prepareEntity(dynamicZibiljaRituals, getCreateIdentifierObject("ZibiljaRitual")), + ...prepareEntity(dynamicMagicalRunes, getCreateIdentifierObject("MagicalRune")), ].reduce>( (acc, [propertyId, value]) => value >= 10 ? { ...acc, [propertyId]: (acc[propertyId] ?? 0) + 1 } : acc, @@ -170,7 +170,9 @@ export const getHighestRequiredAttributeForSpellwork = ( } const exceptionalSkillBonus = - type === undefined ? 0 : countOptions(exceptionalSkill, { type, value: staticSpellwork.id }) + type === undefined + ? 0 + : countOptions(exceptionalSkill, createIdentifierObject(type, staticSpellwork.id)) return { id: singleHighestAttributeId, diff --git a/src/shared/domain/rated/spellActive.ts b/src/shared/domain/rated/spellActive.ts index db49b770..b9bdc74f 100644 --- a/src/shared/domain/rated/spellActive.ts +++ b/src/shared/domain/rated/spellActive.ts @@ -20,6 +20,7 @@ import { TinyActivatable, isTinyActivatableActive, } from "../activatable/activatableEntry.ts" +import { ImprovementCost } from "../adventurePoints/improvementCost.ts" import { FilterApplyingRatedDependencies } from "../dependencies/filterApplyingDependencies.ts" import { GetById } from "../getTypes.ts" import { getHighestAttributeValue } from "./attribute.ts" @@ -45,6 +46,7 @@ import { export type DisplayedActiveCantrip = { kind: "cantrip" static: Cantrip + dynamic: TinyActivatable isUnfamiliar: boolean } @@ -56,7 +58,7 @@ export type DisplayedActiveCantrip = { export type DisplayedActiveSpell = { kind: "spell" static: Spell - dynamic: ActivatableRatedWithEnhancements + dynamic: ActiveActivatableRatedWithEnhancements minimum: number | undefined maximum: number isIncreasable: boolean @@ -72,7 +74,7 @@ export type DisplayedActiveSpell = { export type DisplayedActiveRitual = { kind: "ritual" static: Ritual - dynamic: ActivatableRatedWithEnhancements + dynamic: ActiveActivatableRatedWithEnhancements minimum: number | undefined maximum: number isIncreasable: boolean @@ -83,7 +85,7 @@ export type DisplayedActiveRitual = { type DisplayedActiveMagicalActionBase = { kind: Kind static: Static - dynamic: ActivatableRated + dynamic: ActiveActivatableRated minimum: number | undefined maximum: number isIncreasable: boolean @@ -152,10 +154,16 @@ export type DisplayedActiveJesterTrick = DisplayedActiveMagicalActionBase< * entry, extended by value bounds, and full logic for if the value can be * increased or decreased. */ -export type DisplayedActiveAnimistPower = DisplayedActiveMagicalActionBase< - "animistPower", - AnimistPower -> +export type DisplayedActiveAnimistPower = { + kind: "animistPower" + static: AnimistPower + dynamic: ActiveActivatableRated + minimum: number | undefined + maximum: number + isIncreasable: boolean + isDecreasable: boolean + improvementCost: ImprovementCost +} /** * A combination of a static and corresponding dynamic active geode ritual @@ -210,13 +218,21 @@ export const getVisibleActiveCantrips = ( ): DisplayedActiveCantrip[] => dynamicCantrips .filter(isTinyActivatableActive) - .map(dynamicCantrip => getStaticCantripById(dynamicCantrip.id)) + .map((dynamicCantrip): DisplayedActiveCantrip | undefined => { + const staticCantrip = getStaticCantripById(dynamicCantrip.id) + + if (staticCantrip === undefined) { + return undefined + } + + return { + kind: "cantrip", + static: staticCantrip, + dynamic: dynamicCantrip, + isUnfamiliar: getIsUnfamiliar(dynamicCantrip.id), + } + }) .filter(isNotNullish) - .map(staticCantrip => ({ - kind: "cantrip", - static: staticCantrip, - isUnfamiliar: getIsUnfamiliar(staticCantrip.id), - })) /** * Returns all active spellworks with their corresponding dynamic diff --git a/src/shared/domain/rated/spellActiveBounds.ts b/src/shared/domain/rated/spellActiveBounds.ts index 6ab5336d..a690af71 100644 --- a/src/shared/domain/rated/spellActiveBounds.ts +++ b/src/shared/domain/rated/spellActiveBounds.ts @@ -11,7 +11,9 @@ import { SkillCheck } from "optolith-database-schema/types/_SkillCheck" import { filterNonNullable } from "../../utils/array.ts" import { assertExhaustive } from "../../utils/typeSafety.ts" import { Activatable, countOptions } from "../activatable/activatableEntry.ts" -import { RatedDependency, flattenMinimumRestrictions } from "./ratedDependency.ts" +import { FilterApplyingRatedDependencies } from "../dependencies/filterApplyingDependencies.ts" +import { createIdentifierObject } from "../identifier.ts" +import { flattenMinimumRestrictions } from "./ratedDependency.ts" import { ActivatableRated } from "./ratedEntry.ts" const getSpellworkMinimumFromPropertyKnowledgePrerequistes = ( @@ -35,7 +37,7 @@ export const getSpellworkMinimum = ( activePropertyKnowledges: number[], staticSpellwork: { property: PropertyReference }, dynamicSpellwork: ActivatableRated, - filterApplyingDependencies: (dependencies: RatedDependency[]) => RatedDependency[], + filterApplyingDependencies: FilterApplyingRatedDependencies, ): number | undefined => { const minimumValues = filterNonNullable([ ...flattenMinimumRestrictions(filterApplyingDependencies(dynamicSpellwork.dependencies)), @@ -83,10 +85,10 @@ export const getSpellworkMaximum = ( switch (type) { case "Spell": case "Ritual": - return countOptions(exceptionalSkill, { - type, - value: staticSpellwork.id, - }) + return countOptions( + exceptionalSkill, + createIdentifierObject(type, staticSpellwork.id), + ) case "Curse": case "ElvenMagicalSong": case "DominationRitual": @@ -96,6 +98,7 @@ export const getSpellworkMaximum = ( case "AnimistPower": case "GeodeRitual": case "ZibiljaRitual": + case "MagicalRune": return 0 default: return assertExhaustive(type) diff --git a/src/shared/domain/rated/spellInactive.ts b/src/shared/domain/rated/spellInactive.ts index 467943a0..f3bcdf12 100644 --- a/src/shared/domain/rated/spellInactive.ts +++ b/src/shared/domain/rated/spellInactive.ts @@ -27,6 +27,7 @@ import { import { All, GetById } from "../getTypes.ts" import { MagicalTraditionIdentifier } from "../identifier.ts" import { checkPrerequisitesOfSpellwork } from "../prerequisites/fullPrerequisiteValidationForType.ts" +import { getImprovementCostForAnimistPower } from "./animistPower.ts" import { ActivatableRated, ActivatableRatedWithEnhancements, @@ -122,10 +123,13 @@ export type DisplayedInactiveJesterTrick = DisplayedInactiveMagicalActionBase< /** * A combination of a static and corresponding dynamic inactive animist power entry, extended by whether the entry can be activated. */ -export type DisplayedInactiveAnimistPower = DisplayedInactiveMagicalActionBase< - "animistPower", - AnimistPower -> +export type DisplayedInactiveAnimistPower = { + kind: "animistPower" + static: AnimistPower + dynamic: ActivatableRated | undefined + isAvailable: boolean + improvementCost: ImprovementCost +} /** * A combination of a static and corresponding dynamic inactive geode ritual entry, extended by whether the entry can be activated. @@ -558,6 +562,8 @@ export const getInactiveAnimistPowers = ( activeMagicalTraditions: CombinedActiveMagicalTradition[], isMaximumCountReached: boolean, isEntryAvailable: (src: PublicationRefs) => boolean, + getStaticPatronById: GetById.Static.Patron, + traditionAnimists: Activatable, ): DisplayedInactiveAnimistPower[] => getInactiveMagicalActions( "animistPower", @@ -568,6 +574,18 @@ export const getInactiveAnimistPowers = ( isEntryAvailable, MagicalTraditionIdentifier.Animisten, ) + .map(animistPower => ({ + ...animistPower, + improvementCost: getImprovementCostForAnimistPower( + getStaticPatronById, + traditionAnimists, + animistPower.static.improvement_cost, + ), + })) + .filter( + (animistPower): animistPower is DisplayedInactiveAnimistPower => + animistPower.improvementCost !== undefined, + ) /** * Returns all geode rituals with their corresponding dynamic entries, extended diff --git a/src/shared/domain/sex.ts b/src/shared/domain/sex.ts index 2af793f1..b469443b 100644 --- a/src/shared/domain/sex.ts +++ b/src/shared/domain/sex.ts @@ -1,3 +1,5 @@ +import { ProfessionIdentifier } from "optolith-database-schema/types/_Identifier" +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" import { BinarySex as RawBinarySex } from "optolith-database-schema/types/_Sex" import { SexualCharacteristic } from "optolith-database-schema/types/prerequisites/single/SexualCharacteristicPrerequisite" import { assertExhaustive } from "../utils/typeSafety.ts" @@ -165,3 +167,21 @@ export const matchSexualCharacteristicToSex = ( return assertExhaustive(sexualCharacteristic) } } + +/** + * Defines a dependency on the character’s sex + */ +export type SexDependency = { + value: + | { + kind: "BinarySex" + value: RawBinarySex + } + | { + kind: "SexualCharacteristic" + value: SexualCharacteristic + } + sourceId: ProfessionIdentifier | ActivatableIdentifier + index: number + isPartOfDisjunction: boolean +} diff --git a/src/shared/domain/socialStatus.ts b/src/shared/domain/socialStatus.ts index ad31b62c..9dcd8336 100644 --- a/src/shared/domain/socialStatus.ts +++ b/src/shared/domain/socialStatus.ts @@ -1,6 +1,11 @@ +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" + /** * A dependency on a social status. */ export type SocialStatusDependency = { id: number + sourceId: ActivatableIdentifier + index: number + isPartOfDisjunction: boolean } diff --git a/src/shared/domain/sources/publicationDependency.ts b/src/shared/domain/sources/publicationDependency.ts new file mode 100644 index 00000000..19febd34 --- /dev/null +++ b/src/shared/domain/sources/publicationDependency.ts @@ -0,0 +1,27 @@ +import { PublicationIdentifier } from "optolith-database-schema/types/_Identifier" + +/** + * A dependency on a publication. + */ +export type PublicationDependency = Readonly<{ + /** + * The identifier of the dependency source. + */ + sourceId: PublicationIdentifier + + /** + * The top-level index of the prerequisite. If the prerequisite is part of a + * group or disjunction, this is the index of the group or disjunction. + */ + index: number + + /** + * Is the source prerequisite part of a prerequisite disjunction? + */ + isPartOfDisjunction: boolean + + /** + * The identifier of the required publication. + */ + id: number +}> diff --git a/src/shared/domain/state.ts b/src/shared/domain/state.ts index be04bfdf..fd87ffc0 100644 --- a/src/shared/domain/state.ts +++ b/src/shared/domain/state.ts @@ -1,3 +1,4 @@ +import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup" import { GetById } from "./getTypes.ts" /** @@ -13,8 +14,31 @@ export type StateInstance = { * Is the state active? */ active: boolean + + /** + * The dependencies on the state. + */ + dependencies: StateDependency[] } +/** + * A dependency on a state. + */ +export type StateDependency = { + sourceId: ActivatableIdentifier + index: number + isPartOfDisjunction: boolean +} + +/** + * Creates an instance of a state. + */ +export const createState = (id: number): StateInstance => ({ + id, + active: false, + dependencies: [], +}) + /** * Checks if a state is active. */