refactor!: migrate advantages and disadvantages functionality

This commit is contained in:
Lukas Obermann
2024-02-29 18:35:47 +01:00
parent 94743570aa
commit 8fb0542fe9
116 changed files with 8842 additions and 6261 deletions
+16 -13
View File
@@ -18,7 +18,7 @@
"moment": "^2.29.4",
"mousetrap": "1.6.5",
"optolith-character-schema": "^0.0.3",
"optolith-database-schema": "^0.15.1",
"optolith-database-schema": "^0.16.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.0",
@@ -8726,9 +8726,9 @@
"integrity": "sha512-65wA26OjrFjvtCMaAieLZl2TMjYBx63E0nWRrZyWAzT0gjZ/FI8G8X7ouEF2EuTWYM2YYQMblq8/fK+PhGtWug=="
},
"node_modules/optolith-database-schema": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.15.1.tgz",
"integrity": "sha512-Fpy2JklGxP12kSKSm8naySkpCdj++pfAnhRSmdQ4JIDIjMuhfQWfasFsf4xOIVSkpS1HL2uixxZldPVhXA7XZA==",
"version": "0.16.5",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.16.5.tgz",
"integrity": "sha512-laG0Cpx8yuwGpoCTr0pWE7xnl2YH5iI7dhPFPl8xjnM7E6vs7zXan7/6GJYbZLZBqSyDn6HU/xmvpYKpV2TUlg==",
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@@ -11899,9 +11899,12 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yaml": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz",
"integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
"integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
@@ -18267,9 +18270,9 @@
"integrity": "sha512-65wA26OjrFjvtCMaAieLZl2TMjYBx63E0nWRrZyWAzT0gjZ/FI8G8X7ouEF2EuTWYM2YYQMblq8/fK+PhGtWug=="
},
"optolith-database-schema": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.15.1.tgz",
"integrity": "sha512-Fpy2JklGxP12kSKSm8naySkpCdj++pfAnhRSmdQ4JIDIjMuhfQWfasFsf4xOIVSkpS1HL2uixxZldPVhXA7XZA==",
"version": "0.16.5",
"resolved": "https://registry.npmjs.org/optolith-database-schema/-/optolith-database-schema-0.16.5.tgz",
"integrity": "sha512-laG0Cpx8yuwGpoCTr0pWE7xnl2YH5iI7dhPFPl8xjnM7E6vs7zXan7/6GJYbZLZBqSyDn6HU/xmvpYKpV2TUlg==",
"requires": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@@ -20605,9 +20608,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yaml": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz",
"integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
"integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ=="
},
"yargs-parser": {
"version": "20.2.7",
+1 -1
View File
@@ -20,7 +20,7 @@
"moment": "^2.29.4",
"mousetrap": "1.6.5",
"optolith-character-schema": "^0.0.3",
"optolith-database-schema": "^0.15.1",
"optolith-database-schema": "^0.16.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.0",
@@ -1,65 +0,0 @@
import { set } from "../../../../Data/Lens"
import { List } from "../../../../Data/List"
import { Just, Nothing } from "../../../../Data/Maybe"
import { makeLenses } from "../../../../Data/Record"
import { DisadvantageId } from "../../../Constants/Ids"
import { ActiveObjectWithId } from "../../../Models/ActiveEntries/ActiveObjectWithId"
import { ActivatableActivationValidation } from "../../../Models/View/ActivatableActivationValidationObject"
import { ActivatableCombinedName } from "../../../Models/View/ActivatableCombinedName"
import { ActivatableNameCost } from "../../../Models/View/ActivatableNameCost"
import { ActiveActivatable } from "../../../Models/View/ActiveActivatable"
import { L10n } from "../../../Models/Wiki/L10n"
import { StaticData, StaticDataL } from "../../../Models/Wiki/WikiModel"
import { composeL } from "../../compose"
import { compressList } from "../activatableNameUtils"
const L10nL = makeLenses (L10n)
const StaticDataMockup = set (composeL (StaticDataL.ui, L10nL.id)) ("de-DE") (StaticData.default)
test ("compressList", () => {
expect (compressList (StaticDataMockup)
(List (
ActiveActivatable ({
nameAndCost: ActivatableNameCost ({
active: ActiveObjectWithId ({
id: DisadvantageId.PersonalityFlaw,
index: Nothing,
}),
finalCost: Nothing,
naming: ActivatableCombinedName ({
addName: Just ("Arroganz"),
baseName: "Persönlichkeitsschwäche",
name: "Persönlichkeitsschwäche (Arroganz)",
}),
isAutomatic: false,
}),
validation: ActivatableActivationValidation ({
disabled: Nothing,
}),
heroEntry: Nothing,
wikiEntry: Nothing,
}),
ActiveActivatable ({
nameAndCost: ActivatableNameCost ({
active: ActiveObjectWithId ({
id: DisadvantageId.PersonalityFlaw,
index: Nothing,
}),
finalCost: Nothing,
naming: ActivatableCombinedName ({
addName: Just ("Weltfremd"),
baseName: "Persönlichkeitsschwäche",
name: "Persönlichkeitsschwäche (Weltfremd)",
}),
isAutomatic: false,
}),
validation: ActivatableActivationValidation ({
disabled: Nothing,
}),
heroEntry: Nothing,
wikiEntry: Nothing,
})
)))
.toEqual ("Persönlichkeitsschwäche (Arroganz, Weltfremd)")
})
@@ -1,163 +0,0 @@
/**
* This file provides combining functions for displaying `Activatable`
* entries.
*
* @file src/Utilities/activatableActiveUtils.ts
* @author Lukas Obermann
* @since 1.1.0
*/
import { fmap } from "../../../Data/Functor"
import { List } from "../../../Data/List"
import { bind, liftM2, mapMaybe, Maybe } from "../../../Data/Maybe"
import { lookup, OrderedMap } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { fst, Pair, snd } from "../../../Data/Tuple"
import { ActivatableCategory, Category } from "../../Constants/Categories"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { ActivatableActivationValidation } from "../../Models/View/ActivatableActivationValidationObject"
import { ActivatableCombinedName } from "../../Models/View/ActivatableCombinedName"
import { ActivatableNameCost, ActivatableNameCostSafeCost } from "../../Models/View/ActivatableNameCost"
import { ActiveActivatable } from "../../Models/View/ActiveActivatable"
import { Advantage } from "../../Models/Wiki/Advantage"
import { Disadvantage } from "../../Models/Wiki/Disadvantage"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { WikiEntryByCategory } from "../../Models/Wiki/wikiTypeHelpers"
import { MatchingScriptAndLanguageRelated } from "../../Selectors/activatableSelectors"
import { convertPerTierCostToFinalCost, getCost } from "../AdventurePoints/activatableCostUtils"
import { pipe_ } from "../pipe"
import { getIsRemovalOrChangeDisabled } from "./activatableActiveValidationUtils"
import { getActiveFromState } from "./activatableConvertUtils"
import { getName } from "./activatableNameUtils"
const SDA = StaticData.A
const HA = HeroModel.A
/**
* Takes an Activatable category and a hero and returns the state slice matching
* the passed category.
*/
export const getActivatableHeroSliceByCategory =
(category: ActivatableCategory) =>
(hero: HeroModelRecord): OrderedMap<string, Record<ActivatableDependent>> =>
category === Category.ADVANTAGES
? HA.advantages (hero)
: category === Category.DISADVANTAGES
? HA.disadvantages (hero)
: HA.specialAbilities (hero)
type ActivatableWikiSliceByCategory<A extends ActivatableCategory> =
A extends Category.ADVANTAGES
? Record<Advantage>
: A extends Category.ADVANTAGES
? Record<Disadvantage>
: Record<SpecialAbility>
/**
* Takes an Activatable category and a hero and returns the state slice matching
* the passed category.
*/
export const getActivatableWikiSliceByCategory =
<A extends ActivatableCategory>
(category: A) =>
(wiki: StaticDataRecord): OrderedMap<string, ActivatableWikiSliceByCategory<A>> =>
category === Category.ADVANTAGES
? SDA.advantages (wiki) as OrderedMap<string, ActivatableWikiSliceByCategory<A>>
: category === Category.DISADVANTAGES
? SDA.disadvantages (wiki) as OrderedMap<string, ActivatableWikiSliceByCategory<A>>
: SDA.specialAbilities (wiki) as OrderedMap<string, ActivatableWikiSliceByCategory<A>>
/**
* Returns name, splitted and combined, as well as the AP you get when removing
* the ActiveObject.
* @param obj The ActiveObject with origin id.
* @param wiki The wiki state.
* @param state The current hero's state.
* @param isEntryToAdd If the cost are going to be added or removed from AP left.
* @param locale The locale-dependent messages.
*/
export const getNameCost =
(isEntryToAdd: boolean) =>
(automatic_advantages: List<string>) =>
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(entry: Record<ActiveObjectWithId>): Maybe<Record<ActivatableNameCost>> =>
liftM2 ((finalCost: Pair<number | List<number>, boolean>) =>
(naming: Record<ActivatableCombinedName>) =>
ActivatableNameCost ({
naming,
active: entry,
finalCost: fst (finalCost),
isAutomatic: snd (finalCost),
}))
(getCost (isEntryToAdd) (automatic_advantages) (staticData) (hero) (entry))
(getName (staticData) (entry))
/**
* Returns name, splitted and combined, as well as the AP you get when removing
* the ActiveObject.
* @param obj The ActiveObject with origin id.
* @param wiki The wiki state.
* @param l10n The locale-dependent messages.
*/
export const getNameCostForWiki =
(staticData: StaticDataRecord) =>
(active: Record<ActiveObjectWithId>): Maybe<Record<ActivatableNameCost>> =>
liftM2 ((finalCost: Pair<number | List<number>, boolean>) =>
(naming: Record<ActivatableCombinedName>) =>
ActivatableNameCost ({
active,
naming,
finalCost: fst (finalCost),
isAutomatic: snd (finalCost),
}))
(getCost (true) (List ()) (staticData) (HeroModel.default) (active))
(getName (staticData) (active))
export const getAllActiveByCategory =
<T extends ActivatableCategory>
(category: T) =>
(addLevelToName: boolean) =>
(automatic_advantages: List<string>) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) =>
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord): List<Record<ActiveActivatable<WikiEntryByCategory[T]>>> => {
const convertCost = convertPerTierCostToFinalCost (addLevelToName) (staticData)
const wiki_slice = getActivatableWikiSliceByCategory (category) (staticData)
const hero_slice = getActivatableHeroSliceByCategory (category) (hero)
type Entry = Maybe<Record<ActiveActivatable<WikiEntryByCategory[T]>>>
return pipe_ (
hero_slice,
getActiveFromState,
mapMaybe ((active: Record<ActiveObjectWithId>): Entry => {
const current_id = ActiveObjectWithId.A.id (active)
return bind (lookup (current_id) (wiki_slice)) (staticEntry =>
bind (lookup (current_id) (hero_slice)) (characterEntry =>
liftM2 ((nameAndCost: Record<ActivatableNameCostSafeCost>) =>
(validation: Record<ActivatableActivationValidation>) =>
ActiveActivatable ({
nameAndCost,
validation,
heroEntry: characterEntry,
wikiEntry: staticEntry,
}))
(fmap (convertCost)
(getNameCost (false)
(automatic_advantages)
(staticData)
(hero)
(active)))
(getIsRemovalOrChangeDisabled (staticData)
(hero)
(matchingScriptAndLanguageRelated)
(active)) as Entry))
})
)
}
@@ -1,770 +0,0 @@
/**
* Contains helper functions for calculating restrictions of changing active
* `Activatables`: Minimum level, maximum level and if the entry can be removed.
*
* @author Lukas Obermann
*/
import { not, notP } from "../../../Data/Bool"
import { equals } from "../../../Data/Eq"
import { flip, Functn, thrush } from "../../../Data/Function"
import { fmap } from "../../../Data/Functor"
import { all, any, countWith, elem, filter, find, flength, foldl, isList, List, mapByIdKeyMap, notElemF, notNull, sdelete } from "../../../Data/List"
import { alt, bind, bindF, catMaybes, ensure, fromJust, fromMaybe, isJust, isNothing, Just, liftM2, mapMaybe, Maybe, maybe, Nothing, or, sum } from "../../../Data/Maybe"
import { add, gt, gte, inc, lt, lte, max, min, subtract, subtractBy } from "../../../Data/Num"
import { elems, isOrderedMap, lookup, lookupF, OrderedMap } from "../../../Data/OrderedMap"
import { size } from "../../../Data/OrderedSet"
import { Record } from "../../../Data/Record"
import { MagicalGroup, SpecialAbilityGroup } from "../../Constants/Groups"
import { AdvantageId, DisadvantageId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDependent, isActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActivatableSkillDependent } from "../../Models/ActiveEntries/ActivatableSkillDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { ActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId"
import { DependencyObject } from "../../Models/ActiveEntries/DependencyObject"
import { SkillDependent } from "../../Models/ActiveEntries/SkillDependent"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { ActivatableDependency, Dependent } from "../../Models/Hero/heroTypeHelpers"
import { Pact } from "../../Models/Hero/Pact"
import { TransferUnfamiliar, UnfamiliarGroup } from "../../Models/Hero/TransferUnfamiliar"
import { ActivatableActivationValidation } from "../../Models/View/ActivatableActivationValidationObject"
import { Advantage } from "../../Models/Wiki/Advantage"
import { ExperienceLevel } from "../../Models/Wiki/ExperienceLevel"
import { LiturgicalChant } from "../../Models/Wiki/LiturgicalChant"
import { RequireActivatable } from "../../Models/Wiki/prerequisites/ActivatableRequirement"
import { SocialPrerequisite } from "../../Models/Wiki/prerequisites/SocialPrerequisite"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { Spell } from "../../Models/Wiki/Spell"
import { SelectOption } from "../../Models/Wiki/sub/SelectOption"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { Activatable, EntryWithCategory, LevelAwarePrerequisites, PrerequisitesWithIds, SID } from "../../Models/Wiki/wikiTypeHelpers"
import { MatchingScriptAndLanguageRelated } from "../../Selectors/activatableSelectors"
import { countActiveGroupEntries } from "../entryGroupUtils"
import { getAllEntriesByGroup, getHeroStateItem } from "../heroStateUtils"
import { isOwnTradition } from "../Increasable/liturgicalChantUtils"
import { ensure as newEnsure, toNewMaybe } from "../Maybe"
import { pipe, pipe_ } from "../pipe"
import { flattenPrerequisites } from "../Prerequisites/flattenPrerequisites"
import { setPrerequisiteId } from "../Prerequisites/setPrerequisiteId"
import { validateLevel, validateObject } from "../Prerequisites/validatePrerequisitesUtils"
import { isBoolean, isNumber, misNumberM, misStringM } from "../typeCheckUtils"
import { getWikiEntry, isActivatableWikiEntry } from "../WikiUtils"
import { countActiveSkillEntries } from "./activatableSkillUtils"
import { isStyleValidToRemove } from "./ExtendedStyleUtils"
import { isActive } from "./isActive"
import { findSelectOption, getActiveSelections } from "./selectionUtils"
import { getBlessedTraditionFromWiki, getMagicalTraditionsHeroEntries, isBlessedTradId, isMagicalTradId } from "./traditionUtils"
const hasRequiredMinimumLevel =
(min_level: Maybe<number>) => (max_level: Maybe<number>): boolean =>
isJust (max_level) && isJust (min_level)
const HA = HeroModel.A
const SDA = StaticData.A
const ELA = ExperienceLevel.A
const SOA = SelectOption.A
const AAL = Advantage.AL
const ADA = ActivatableDependent.A
const ASDA = ActivatableSkillDependent.A
const DOA = DependencyObject.A
const AOA = ActiveObject.A
const AOWIA = ActiveObjectWithId.A
const RAAL = RequireActivatable.AL
const PA = Pact.A
const SA = Spell.A
const LCA = LiturgicalChant.A
const TUA = TransferUnfamiliar.A
/**
* Checks if you can somehow remove an ActiveObject from the given entry.
*/
const isRemovalDisabledEntrySpecific =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
({
isEntryRequiringMatchingScriptAndLangActive,
scriptsWithMatchingLanguages,
languagesWithMatchingScripts,
languagesWithDependingScripts,
}: MatchingScriptAndLanguageRelated) =>
(wiki_entry: Activatable) =>
(hero_entry: Record<ActivatableDependent>) =>
// tslint:disable-next-line: cyclomatic-complexity
(active: Record<ActiveObjectWithId>): boolean => {
const id = AAL.id (wiki_entry)
const mstart_el =
lookupF (SDA.experienceLevels (wiki))
(HA.experienceLevel (hero))
if (isMagicalTradId (id)) {
// All active tradition entries
const traditions =
getMagicalTraditionsHeroEntries (HA.specialAbilities (hero))
const multiple_traditions = flength (traditions) > 1
// multiple traditions are currently not supported and there must be no
// active spell or cantrip
return multiple_traditions
|| countActiveSkillEntries ("spells") (hero) > 0
|| size (HA.cantrips (hero)) > 0
}
else if (isBlessedTradId (id)) {
// there must be no active liturgical chant or blessing
return countActiveSkillEntries ("liturgicalChants") (hero) > 0
|| size (HA.blessings (hero)) > 0
}
switch (id) {
case AdvantageId.ExceptionalSkill: {
// value of target skill
const mvalue =
pipe_ (
active,
AOWIA.sid,
misStringM,
bindF (lookupF (HA.skills (hero))),
fmap (SkillDependent.AL.value)
)
// amount of active Exceptional Skill advantages for the same skill
const counter = countWith (pipe (AOA.sid, equals (AOWIA.sid (active))))
(ADA.active (hero_entry))
// if the maximum value is reached removal needs to be disabled
return or (liftM2 (gte)
(mvalue)
(fmap (pipe (ELA.maxSkillRating, add (counter)))
(mstart_el)))
}
case AdvantageId.ExceptionalCombatTechnique: {
// value of target combat technique
const value =
pipe_ (
active,
AOWIA.sid,
misStringM,
bindF (lookupF (HeroModel.A.combatTechniques (hero))),
maybe (6) (SkillDependent.A.value)
)
// if the maximum value is reached removal needs to be disabled
return maybe (true) (pipe (ELA.maxCombatTechniqueRating, inc, lte (value))) (mstart_el)
}
case SpecialAbilityId.Literacy: {
return isEntryRequiringMatchingScriptAndLangActive
&& scriptsWithMatchingLanguages.length < 2
&& toNewMaybe (AOWIA.sid (active))
.bind (sid => newEnsure (sid, isNumber))
.maybe (false, sid => scriptsWithMatchingLanguages.includes (sid))
}
case SpecialAbilityId.Language: {
const isRequiredByEntryRequiringMatchingScriptAndLanguage =
isEntryRequiringMatchingScriptAndLangActive
&& languagesWithMatchingScripts.length < 2
&& toNewMaybe (AOWIA.sid (active))
.bind (sid => newEnsure (sid, isNumber))
.maybe (false, sid => languagesWithMatchingScripts.includes (sid))
const isRequiredByDependingLanguage =
toNewMaybe (AOWIA.sid (active))
.bind (sid => newEnsure (sid, isNumber))
.maybe (false, sid =>
languagesWithDependingScripts
.findM (languageGroupOfScript =>
languageGroupOfScript.includes (sid))
.maybe (false, languageGroupOfScript =>
languageGroupOfScript
.mapMaybe (idOfGroup =>
toNewMaybe (find ((ao: Record<ActiveObject>) =>
Maybe.elemF (AOA.sid (ao)) (idOfGroup))
(ADA.active (hero_entry))))
.length < 2))
return isRequiredByEntryRequiringMatchingScriptAndLanguage || isRequiredByDependingLanguage
}
case SpecialAbilityId.PropertyKnowledge:
return pipe_ (
active,
AOWIA.sid,
misNumberM,
maybe (false)
(prop_id => OrderedMap.any ((spell: Record<ActivatableSkillDependent>) =>
ASDA.value (spell) > 14
&& pipe_ (
spell,
ASDA.id,
lookupF (SDA.spells (wiki)),
maybe (true)
(pipe (SA.property, equals (prop_id)))
))
(HA.spells (hero)))
)
case SpecialAbilityId.AspectKnowledge: {
const all_aspcs = getActiveSelections (hero_entry)
return pipe_ (
active,
AOWIA.sid,
misNumberM,
maybe (false)
(aspc_id => {
const other_aspcs = sdelete<string | number> (aspc_id) (all_aspcs)
return OrderedMap.any ((chant: Record<ActivatableSkillDependent>) =>
ASDA.value (chant) > 14
&& pipe_ (
chant,
ASDA.id,
lookupF (SDA.liturgicalChants (wiki)),
maybe (true)
(pipe (
LCA.aspects,
aspcs => elem (aspc_id) (aspcs)
&& all (notElemF (other_aspcs))
(aspcs)
))
))
(HA.liturgicalChants (hero))
})
)
}
case SpecialAbilityId.CombatStyleCombination: {
const armedStyleActive = countActiveGroupEntries (wiki)
(hero)
(SpecialAbilityGroup.CombatStylesArmed)
const unarmedStyleActive = countActiveGroupEntries (wiki)
(hero)
(SpecialAbilityGroup.CombatStylesUnarmed)
const totalActive = armedStyleActive + unarmedStyleActive
// default is 1 per group (armed/unarmed), but with this SA 1 more in
// one group: maximum of 3, but max 2 per group. If max is reached, this
// SA cannot be removed
return totalActive >= 3
|| armedStyleActive >= 2
|| unarmedStyleActive >= 2
}
case SpecialAbilityId.MagicStyleCombination: {
const totalActive = countActiveGroupEntries (wiki) (hero) (13)
// default is 1, but with this SA its 2. If it's 2 this SA is neccessary
// and cannot be removed
return totalActive >= 2
}
// Extended Blessed Special Abilities that allow to learn liturgical
// chants of different traditions
case SpecialAbilityId.Zugvoegel:
case SpecialAbilityId.JaegerinnenDerWeissenMaid:
case SpecialAbilityId.AnhaengerDesGueldenen: {
const mblessed_tradition =
getBlessedTraditionFromWiki (SDA.specialAbilities (wiki))
(HA.specialAbilities (hero))
// Wiki entries for all active liturgical chants
const active_chants =
pipe_ (
hero,
HA.liturgicalChants,
elems,
filter<Record<ActivatableSkillDependent>> (ASDA.active),
mapByIdKeyMap (SDA.liturgicalChants (wiki))
)
// If there are chants active that do not belong to the own tradition
const mactive_unfamiliar_chants =
fmap (pipe (isOwnTradition, notP, any, thrush (active_chants)))
(mblessed_tradition)
return or (mactive_unfamiliar_chants)
}
// entries transferring unfamiliar special abilities (start)
case SpecialAbilityId.TraditionGuildMages:
case SpecialAbilityId.MadaschwesternStil:
case SpecialAbilityId.ScholarDesMagierkollegsZuHoningen:
case SpecialAbilityId.Zaubervariabilitaet:
case SpecialAbilityId.ScholarDerHalleDesLebensZuNorburg:
case SpecialAbilityId.ScholarDesKreisesDerEinfuehlung: {
const m_static_spell_enhancements = pipe_ (
wiki,
SDA.specialAbilities,
lookup<string> (SpecialAbilityId.SpellEnhancement)
)
const active_spell_enhancements = pipe_ (
hero,
HA.specialAbilities,
lookup<string> (SpecialAbilityId.SpellEnhancement),
liftM2 ((static_spell_enhancements: Record<SpecialAbility>) =>
pipe (
ADA.active,
mapMaybe (pipe (
AOA.sid,
misNumberM,
findSelectOption (static_spell_enhancements),
bindF (SOA.target)
))
))
(m_static_spell_enhancements),
fromMaybe (List<string> ())
)
type Target = string | UnfamiliarGroup
const targets =
id === SpecialAbilityId.Zaubervariabilitaet
? List<Target> (UnfamiliarGroup.Spells)
: catMaybes (List (
pipe_ (active, AOWIA.sid, misStringM),
pipe_ (active, AOWIA.sid2, misStringM),
pipe_ (active, AOWIA.sid3, misStringM)
))
const target_matches_id =
(target: Target) =>
(spell_id: string) =>
typeof target === "string"
// The target must be the same …
? target === spell_id
// … or it must be from the same category
: target === UnfamiliarGroup.Spells
const relevant_spell_enhancements = pipe_ (
active_spell_enhancements,
filter (spell_id => any (Functn.flip (target_matches_id) (spell_id)) (targets))
)
// spell enhancements must exist to possibly be a dependency
// also, a spell must be selected and it has to have an active spell
// enhancement to be relevant
if (notNull (active_spell_enhancements)
&& notNull (targets)
&& notNull (relevant_spell_enhancements)) {
const transferred = HA.transferredUnfamiliarSpells (hero)
return pipe_ (
relevant_spell_enhancements,
// check for alternative transferred dependency matches for all
// targets that would allow this entry to be removed
all (spell_id => pipe_ (
transferred,
// It cannot be the same entry
any (tu => TUA.srcId (tu) !== id && target_matches_id (TUA.id (tu)) (spell_id))
)),
// if all spell enhancement have alternative, it can be safely
// removed and thus removal is "not" disabled
not
)
}
return false
}
default:
return false
}
}
const isLevelMatched = (required_level_opt: Maybe<number>, active_level_opt: Maybe<number>) =>
maybe (true)
((required_level: number) => maybe (false) (gte (required_level)) (active_level_opt))
(required_level_opt)
const isSidMatched = (
required_sid_opt: Maybe<string | number | List<number>>,
active_sid_opt: Maybe<SID>
) =>
maybe (true)
((required_sid: string | number | List<number>) =>
maybe (false) (equals (required_sid)) (active_sid_opt))
(required_sid_opt)
const isEntryDisabledByDependencies =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(wiki_entry: Activatable) =>
(state_entry: Record<ActivatableDependent>) =>
(current_active: Record<ActiveObjectWithId>): boolean => {
const active_sid_opt = AOWIA.sid (current_active)
const active_sid2_opt = AOWIA.sid2 (current_active)
const active_level_opt = AOWIA.tier (current_active)
const active_num = flength (ADA.active (state_entry))
return pipe_ (
state_entry,
ADA.dependencies,
// if there is any dependency that disables the possibility to remove
// the entry
any (dep => {
if (isBoolean (dep)) {
return dep && active_num < 2
}
// Try to find out if the dependency object targets this entry
const applies_to_sid1 = isSidMatched (DOA.sid (dep), active_sid_opt)
const applies_to_sid2 = isSidMatched (DOA.sid2 (dep), active_sid2_opt)
const applies_to_level = isLevelMatched (DOA.tier (dep), active_level_opt)
if (applies_to_sid1 && applies_to_sid2 && applies_to_level) {
// A Just if the depedency exists because of a list of ids
// in a prerequiste. Contains the source id of the object
// where the prerequisite is from.
const current_origin = DOA.origin (dep)
if (isJust (current_origin)) {
return pipe_ (
fromJust (current_origin),
getWikiEntry (wiki),
bindF<EntryWithCategory, Activatable>
(ensure (isActivatableWikiEntry)),
// Get flat prerequisites for origin entry
fmap ((origin_entry: Activatable) =>
flattenPrerequisites (Nothing)
(alt (AAL.tiers (origin_entry)) (Just (1)))
(AAL.prerequisites (origin_entry))),
// Get the prerequisite that matches this entry
// to get all other options from list
bindF (find ((req): req is PrerequisitesWithIds => {
if (typeof req === "string" || SocialPrerequisite.is (req)) {
return false
}
const current_id = RAAL.id (req)
// the id must be a list of ids
// because otherwise no options in
// terms of fulfilling the
// prerequisite would be possible
return isList (current_id)
// check if the current entry's
// id is actually a member of
// the prerequisite
&& elem (AAL.id (wiki_entry))
(current_id)
})),
// Check if there are other entries that would
// match the prerequisite so that this entry
// could be removed
fmap ((req: PrerequisitesWithIds) =>
!any ((x: string) =>
validateObject (wiki)
(hero)
(setPrerequisiteId (x) (req))
(AAL.id (wiki_entry)))
(sdelete (AAL.id (wiki_entry))
(RAAL.id (req) as List<string>))),
or
)
}
return pipe_ (
state_entry,
ADA.active,
countWith (other_active =>
isSidMatched (DOA.sid (dep), AOA.sid (other_active))
&& isSidMatched (DOA.sid2 (dep), AOA.sid2 (other_active))
&& isLevelMatched (DOA.tier (dep), AOA.tier (other_active))),
// If multiple entries match the dependency, the current one must be
// one of them, another one can fulfill it so that the current one
// can be safely removed
lt (2)
)
}
return false
})
)
}
const isStyleSpecialAbilityRemovalDisabled =
(hero: HeroModelRecord) =>
(wiki_entry: Activatable): boolean =>
SpecialAbility.is (wiki_entry)
&& !isStyleValidToRemove (hero)
(Just (wiki_entry))
export const getMinLevelForIncreaseEntry: (def: number) => (count: number) => Maybe<number> =
// the entry allows to have more entries, which would not be possible without.
// The minimum is simply the count - def, because if there are def + 1
// entries, it must be at least 1, if there are def + 2 entries,
// it must be at least 2. And if the count is not greater than def, the
// increase entry is not used so we dont need a restriction
def => pipe (ensure (gt (def)), fmap (subtractBy (def)))
export const getMaxLevelForDecreaseEntry: (def: number) => (count: number) => Maybe<number> =
// the more entries the user buys, the less levels are
// possible. If the user has 3 or more entries, the decrease entry cannot
// be used at all. In those cases (which should not happen), the maximum
// will be 0.
def => pipe (subtract (def), max (0), Just)
export const getSermonsAndVisionsCount =
(wiki: StaticDataRecord) =>
(state: HeroModelRecord):
(gr: number) => number =>
pipe (
getAllEntriesByGroup (SDA.specialAbilities (wiki))
(HA.specialAbilities (state)),
countWith (isActive)
)
const getEntrySpecificMinimumLevel =
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
({
isEntryRequiringMatchingScriptAndLangActive,
scriptsWithMatchingLanguages,
languagesWithMatchingScripts,
}: MatchingScriptAndLanguageRelated) =>
(x: Record<ActiveObjectWithId>): Maybe<number> => {
switch (AOWIA.id (x)) {
case AdvantageId.LargeSpellSelection:
return pipe_ (
hero,
countActiveSkillEntries ("spells"),
getMinLevelForIncreaseEntry (3)
)
case AdvantageId.ZahlreichePredigten:
return pipe_ (
24,
getSermonsAndVisionsCount (staticData) (hero),
getMinLevelForIncreaseEntry (3)
)
case AdvantageId.ZahlreicheVisionen:
return pipe_ (
27,
getSermonsAndVisionsCount (staticData) (hero),
getMinLevelForIncreaseEntry (3)
)
case SpecialAbilityId.Literacy: {
return isEntryRequiringMatchingScriptAndLangActive
&& scriptsWithMatchingLanguages.length < 2
&& toNewMaybe (AOWIA.sid (x))
.bind (sid => newEnsure (sid, isNumber))
.maybe (false, sid => scriptsWithMatchingLanguages.includes (sid))
? Just (3)
: Nothing
}
case SpecialAbilityId.Language: {
return isEntryRequiringMatchingScriptAndLangActive
&& languagesWithMatchingScripts.length < 2
&& toNewMaybe (AOWIA.sid (x))
.bind (sid => newEnsure (sid, isNumber))
.maybe (false, sid => languagesWithMatchingScripts.includes (sid))
? Just (3)
: Nothing
}
case SpecialAbilityId.Imitationszauberei:
return pipe_ (
hero,
HA.spells,
elems,
countWith (pipe (
ASDA.id,
lookupF (SDA.spells (staticData)),
maybe (false)
(pipe (SA.gr, gr => gr === MagicalGroup.Spells))
)),
ensure (gt (0))
)
default:
return Nothing
}
}
const getEntrySpecificMaximumLevel =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(entry_id: string): Maybe<number> => {
switch (entry_id) {
case DisadvantageId.SmallSpellSelection:
return pipe_ (hero, countActiveSkillEntries ("spells"), getMaxLevelForDecreaseEntry (3))
case DisadvantageId.WenigePredigten:
return pipe_ (24, getSermonsAndVisionsCount (wiki) (hero), getMaxLevelForDecreaseEntry (3))
case DisadvantageId.WenigeVisionen:
return pipe_ (27, getSermonsAndVisionsCount (wiki) (hero), getMaxLevelForDecreaseEntry (3))
case SpecialAbilityId.DunklesAbbildDerBuendnisgabe:
return pipe_ (hero, HA.pact, fmap (PA.level))
default:
return Nothing
}
}
type MinLevelDepSid = string | number | List<number>
const adjustMinimumLevelByDependencies =
(entry: Record<ActiveObjectWithId>) =>
flip (foldl ((min_level: Maybe<number>): (dep: ActivatableDependency) => Maybe<number> =>
pipe (
// dependency must include a minimum level, which only occurs
// in a DependencyObject
ensure (DependencyObject.is),
// get the level dependency from the object and ensure it's
// greater than the current minimum level and that
bindF (dep => bind (DOA.tier (dep))
// new min must be lower than current
// min level
(ensure (dep_level => sum (min_level) < dep_level
// if the DependencyObject defines a
// sid, too, the entry must match the
// sid as well. A DependencyObject
// without a sid is valid for all
// entries (in case of calculating a
// minimum level)
&& maybe (true)
(flip (Maybe.elem)
<MinLevelDepSid>
(AOWIA.sid (entry)))
(DOA.sid (dep))))),
// if the current dependency's level is not valid, return
// the current minimum
flip (alt) (min_level)
)))
/**
* Get minimum valid tier.
*/
export const getMinTier =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) =>
(entry: Record<ActiveObjectWithId>) =>
(entry_dependencies: List<ActivatableDependency>): Maybe<number> =>
adjustMinimumLevelByDependencies (entry)
(entry_dependencies)
(getEntrySpecificMinimumLevel (wiki)
(hero)
(matchingScriptAndLanguageRelated)
(entry))
const minMaybe: (mx: Maybe<number>) => (my: Maybe<number>) => Maybe<number> =
mx => my => isNothing (mx) ? my : isNothing (my) ? mx : Just (min (fromJust (mx)) (fromJust (my)))
/**
* Get maximum valid tier.
*/
export const getMaxTier =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(entry_prerequisites: LevelAwarePrerequisites) =>
(entry_dependencies: List<ActivatableDependency>) =>
(entry_id: string): Maybe<number> => {
const entry_specific_max_level = getEntrySpecificMaximumLevel (wiki) (hero) (entry_id)
if (isOrderedMap (entry_prerequisites)) {
return minMaybe (entry_specific_max_level)
(validateLevel (wiki)
(hero)
(entry_prerequisites)
(entry_dependencies)
(entry_id))
}
return entry_specific_max_level
}
/**
* Checks if the given ActiveObject can be removed or changed in tier.
* @param obj The ActiveObject with origin id.
* @param state The current hero's state.
*/
export const getIsRemovalOrChangeDisabled =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) =>
(entry: Record<ActiveObjectWithId>): Maybe<Record<ActivatableActivationValidation>> =>
pipe (
getWikiEntry (wiki),
bindF<EntryWithCategory, Activatable> (ensure (isActivatableWikiEntry)),
bindF (
wiki_entry =>
pipe_ (
entry,
AOWIA.id,
getHeroStateItem (hero),
bindF<Dependent, Record<ActivatableDependent>>
(ensure (isActivatableDependent)),
fmap ((hero_entry: Record<ActivatableDependent>) => {
const minimum_level = getMinTier (wiki)
(hero)
(matchingScriptAndLanguageRelated)
(entry)
(ADA.dependencies (hero_entry))
return ActivatableActivationValidation ({
disabled:
// Disable if a minimum level is required
hasRequiredMinimumLevel (minimum_level)
(AAL.tiers (wiki_entry))
// Disable if style special ability is required for
// extended special abilities
|| isStyleSpecialAbilityRemovalDisabled (hero)
(wiki_entry)
// Disable if other entries depend on this entry
|| isEntryDisabledByDependencies (wiki)
(hero)
(wiki_entry)
(hero_entry)
(entry)
// Disable if specific entry conditions disallow
// remove
|| isRemovalDisabledEntrySpecific (wiki)
(hero)
(matchingScriptAndLanguageRelated)
(wiki_entry)
(hero_entry)
(entry),
minLevel: minimum_level,
maxLevel: getMaxTier (wiki)
(hero)
(AAL.prerequisites (wiki_entry))
(ADA.dependencies (hero_entry))
(AOWIA.id (entry)),
})
})
)
)
)
(AOWIA.id (entry))
@@ -1,83 +0,0 @@
// import { Activatable } from "../../Models/Wiki/wikiTypeHelpers";
// import { ActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId";
// import { Record } from "../../../Data/Record";
// import { Maybe } from "../../../Data/Maybe";
// import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent";
// import { HeroModelRecord } from "../../Models/Hero/HeroModel";
// import { addDependencies } from "../Dependencies/dependencyUtils";
// import { getCombinedPrerequisites } from "./activatableActivationUtils";
// import { RequireActivatable, reqToActive, reqToActiveFst } from "../../Models/Wiki/prerequisites/ActivatableRequirement";
// import { pipe } from "../pipe";
// import { isList, head } from "../../../Data/List";
// import { fmap } from "../../../Data/Functor";
// const RAA = RequireActivatable.A
// export const addActivatableDeep =
// (x: Record<RequireActivatable>) =>
// (wiki_entry: Activatable) =>
// (mhero_entry: Maybe<Record<ActivatableDependent>>) =>
// (hero: HeroModelRecord) => {
// const a = reqToActiveFst (x)
// const combined_prereq = getCombinedPrerequisites (true)
// (wiki_entry)
// (mhero_entry)
// (a)
// const curr_ids = RAA.id (x)
// const curr_id = isList (curr_ids) ? head (curr_ids) : curr_ids
// return pipe (
// (hero: HeroModelRecord) =>
// fmap (addDependencies (curr_id)
// (combined_prereq))
// )
// }
// /**
// * Adds or removes active instance and related prerequisites based on passed
// * functions.
// * @param getActive
// * @param changeDependencies
// * @param changeActive
// * @param add If an entry should be added or removed.
// */
// const changeActiveLength =
// (modifyDependencies: typeof addDependencies) =>
// (modifyActiveObjects: ident<List<Record<ActiveObject>>>) =>
// (add: boolean) =>
// (entry: Record<ActiveObject>) =>
// (wiki_entry: Activatable) =>
// (mhero_entry: Maybe<Record<ActivatableDependent>>) =>
// (hero: HeroModelRecord) =>
// // Source id
// modifyDependencies (id (wiki_entry))
// // get the prerequisites that need to be applied as
// // dependencies to all objects the activation or
// // deactivation depends on
// (getCombinedPrerequisites (add)
// (wiki_entry)
// (mhero_entry)
// (entry))
// // modify the list of `ActiveObjects` and pass the hero
// // to `changeDependencies` so that it can apply all the
// // dependencies to the updated hero
// (adjustEntryDef (over (ActivatableDependentL.active) (modifyActiveObjects))
// (id (wiki_entry))
// (hero))
// /**
// * Activates the entry with the given parameters and adds all needed
// * dependencies.
// * @param entry The `ActiveObject`.
// */
// export const activateByObject =
// (entry: Record<ActiveObject>) =>
// changeActiveLength (addDependencies)
// (consF (entry))
// (true)
// (entry)
@@ -1,101 +0,0 @@
/**
* This file provides several helper functions for working with `Activatable`s.
*
* @file src/Utilities/activatableConvertUtils.ts
* @author Lukas Obermann
* @since 1.1.0
*/
import { fmap } from "../../../Data/Functor"
import { append, elemF, empty, imap, List } from "../../../Data/List"
import { alt, guard, isJust, Nothing, or, then } from "../../../Data/Maybe"
import { foldr } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { AdvantageId, DisadvantageId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableActivationOptions } from "../../Models/Actions/ActivatableActivationOptions"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { toActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId"
import { pipe, pipe_ } from "../pipe"
const ADA = ActivatableDependent.A
const AAOA = ActivatableActivationOptions.A
/**
* Converts the object generated by the list item to an object that can be
* inserted into an array of ActiveObjects.
* @param obj The entry for which you want to convert the object.
* @param activate The object generated by the list item.
*/
export const convertUIStateToActiveObject =
(activate: Record<ActivatableActivationOptions>): Record<ActiveObject> => {
const id = AAOA.id (activate)
const selectOptionId1 = AAOA.selectOptionId1 (activate)
const selectOptionId2 = AAOA.selectOptionId2 (activate)
const selectOptionId3 = AAOA.selectOptionId3 (activate)
const input = AAOA.input (activate)
const level = AAOA.level (activate)
const customCost = AAOA.customCost (activate)
return id === AdvantageId.HatredOf
? ActiveObject ({
sid: selectOptionId1,
sid2: input,
cost: customCost,
})
: id === DisadvantageId.PersonalityFlaw
? ActiveObject ({
sid: selectOptionId1,
sid2: or (fmap (elemF (List<number | string> (7, 8)))
(selectOptionId1))
? input
: Nothing,
cost: customCost,
})
: id === SpecialAbilityId.SkillSpecialization
? ActiveObject ({
sid: selectOptionId1,
sid2: alt<number | string> (input)
(selectOptionId2),
cost: customCost,
})
: isJust (input) && isJust (selectOptionId1)
? ActiveObject ({
sid: selectOptionId1,
sid2: input,
sid3: selectOptionId2,
tier: level,
cost: customCost,
})
: ActiveObject ({
sid: alt<number | string> (input)
(selectOptionId1),
sid2: then (guard (isJust (input) || isJust (selectOptionId1)))
(selectOptionId2),
sid3: selectOptionId3,
tier: level,
cost: customCost,
})
}
/**
* Generates a list of ActiveObjects based on the given instance.
*/
export const convertActivatableToArray =
(x: Record<ActivatableDependent>) =>
pipe_ (
x,
ADA.active,
imap (index => toActiveObjectWithId (index) (ADA.id (x)))
)
/**
* Get all active items in an array.
* @param state A state slice.
*/
export const getActiveFromState =
foldr (pipe (convertActivatableToArray, append)) (empty)
export interface ActiveObjectAny extends ActiveObject {
[key: string]: any
}
@@ -1,955 +0,0 @@
/**
* Get the needed options for `Activatable` entries that are available to
* activate.
*
* @file src/Utilities/activatableInactiveUtils.ts
* @author Lukas Obermann
* @since 1.1.0
*/
import { notP } from "../../../Data/Bool"
import { equals, notEquals } from "../../../Data/Eq"
import { cnst, ident } from "../../../Data/Function"
import { fmap, fmapF, mapReplace } from "../../../Data/Functor"
import { over, set } from "../../../Data/Lens"
import { consF, countWith, elem, elemF, filter, find, flength, fnull, foldr, isList, List, map, mapByIdKeyMap, notElem, notElemF, notNull, nub, subscript } from "../../../Data/List"
import { all, ap, bind, bindF, ensure, fromJust, fromMaybe, guard, isJust, isNothing, join, Just, liftM2, listToMaybe, Maybe, maybe, maybeToList, Nothing, or, thenF } from "../../../Data/Maybe"
import { add, gt, gte, inc, multiply } from "../../../Data/Num"
import { alter, elems, foldrWithKey, isOrderedMap, lookup, lookupF, member, OrderedMap } from "../../../Data/OrderedMap"
import { Record, RecordI } from "../../../Data/Record"
import { filterMapListT, filterT, mapT } from "../../../Data/Transducer"
import { traceShowId } from "../../../Debug/Trace"
import { Aspect, CombatTechniqueGroupId, MagicalTradition, SkillGroup, SpecialAbilityGroup } from "../../Constants/Groups"
import { AdvantageId, DisadvantageId, SkillId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActivatableSkillDependent } from "../../Models/ActiveEntries/ActivatableSkillDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { SkillDependent } from "../../Models/ActiveEntries/SkillDependent"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { Pact } from "../../Models/Hero/Pact"
import { Rules } from "../../Models/Hero/Rules"
import { AdventurePointsCategories } from "../../Models/View/AdventurePointsCategories"
import { InactiveActivatable, InactiveActivatableL } from "../../Models/View/InactiveActivatable"
import { Advantage } from "../../Models/Wiki/Advantage"
import { LiturgicalChant } from "../../Models/Wiki/LiturgicalChant"
import { Skill } from "../../Models/Wiki/Skill"
import { Spell } from "../../Models/Wiki/Spell"
import { Application } from "../../Models/Wiki/sub/Application"
import { SelectOption, SelectOptionL } from "../../Models/Wiki/sub/SelectOption"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { Activatable } from "../../Models/Wiki/wikiTypeHelpers"
import { MatchingScriptAndLanguageRelated } from "../../Selectors/activatableSelectors"
import { composeT } from "../compose"
import { filterUnfamiliar } from "../Dependencies/TransferredUnfamiliarUtils"
import { countActiveGroupEntries } from "../entryGroupUtils"
import { getAllEntriesByGroup } from "../heroStateUtils"
import { getAPForActivatation } from "../ImprovementCost"
import { getTraditionOfAspect } from "../Increasable/liturgicalChantUtils"
import { isUnfamiliarSpell } from "../Increasable/spellUtils"
import { ensure as newensure, toNewMaybe } from "../Maybe"
import { pipe, pipe_ } from "../pipe"
import { validateLevel, validatePrerequisites } from "../Prerequisites/validatePrerequisitesUtils"
import { isEntryAvailable } from "../RulesUtils"
import { sortRecordsByName } from "../sortBy"
import { isNumber, isString, isStringM, misNumberM, misStringM } from "../typeCheckUtils"
import { getMaxLevelForDecreaseEntry, getSermonsAndVisionsCount } from "./activatableActiveValidationUtils"
import { isAdditionDisabled } from "./activatableInactiveValidationUtils"
import { modifyByLevel } from "./activatableModifierUtils"
import { countActiveSkillEntries } from "./activatableSkillUtils"
import { isMaybeActive } from "./isActive"
import { getActiveSecondarySelections, getActiveSelections, getActiveSelectionsMaybe, getRequiredSelections } from "./selectionUtils"
import { getBlessedTradition, getMagicalTraditionsHeroEntries, mapBlessedNumIdToTradId } from "./traditionUtils"
const SDA = StaticData.A
const HA = HeroModel.A
const AAL = Advantage.AL
const LCA = LiturgicalChant.A
const SpA = Spell.A
const ADA = ActivatableDependent.A
const ASDA = ActivatableSkillDependent.A
const SkDA = SkillDependent.A
const SOA = SelectOption.A
const AppA = Application.A
const AOA = ActiveObject.A
const SpAL = Spell.AL
const SA = Skill.A
const RA = Rules.A
const SOL = SelectOptionL
const IAA = InactiveActivatable.A
const IAL = InactiveActivatableL
const PA = Pact.A
const APCA = AdventurePointsCategories.A
/**
* `isNotActive :: Maybe ActivatableDependent -> SelectOption -> Bool`
*
* Test if the id of the passed select option is activated for the passed
* `ActivatableDependent`.
*/
const isNotActive =
pipe (
getActiveSelectionsMaybe,
fromMaybe<List<string | number>> (List.empty),
activeSelections => pipe (
SOA.id,
notElemF (activeSelections)
)
)
/**
* `areNoSameActive :: Maybe ActivatableDependent -> SelectOption -> Bool`
*
* Test if a select option is not activated more than once for the passed
* `ActivatableDependent`.
*/
const areNoSameActive =
pipe (
getActiveSelectionsMaybe,
fromMaybe<List<string | number>> (List.empty),
activeSelections => pipe (
SOA.id,
current_id => countWith (equals (current_id)) (activeSelections) < 2
)
)
/**
* `isNotRequired :: Maybe ActivatableDependent -> SelectOption -> Bool`
*
* Test if the id of the passed select option is required for the passed
* `ActivatableDependent`.
*/
const isNotRequired =
pipe (
getRequiredSelections,
fromMaybe<List<string | number | List<number>>> (List.empty),
requiredSelections => pipe (
SOA.id,
notElemF (requiredSelections)
)
)
/**
* `isNotRequiredNotActive :: Maybe ActivatableDependent -> SelectOption -> Bool`
*
* Test if the id of the passed select option is neither required nor activated
* for the passed `ActivatableDependent`.
*/
const isNotRequiredNotActive =
(mhero_entry: Maybe<Record<ActivatableDependent>>) => {
const isNoActiveSelection = isNotActive (mhero_entry)
const isNoRequiredSelection = isNotRequired (mhero_entry)
return (e: Record<SelectOption>) =>
isNoActiveSelection (e) && isNoRequiredSelection (e)
}
/**
* Increment the value at the specified key by `1`. If there is no value at that
* key, the value will be set to `0`.
*/
const incMapVal = alter (pipe (maybe (1) (inc), Just))
const addChantToCounter =
(chant: Record<LiturgicalChant>) =>
(mp: OrderedMap<number, number>) =>
foldr ((aspect: Aspect) => OrderedMap.insertWith (add) <number> (aspect) (1))
(mp)
(LCA.aspects (chant))
const addSpellToCounter = pipe (SpA.property, incMapVal)
const filterSkillsGte10 = filter (pipe (ASDA.value, gte (10)))
const foldCounter =
foldrWithKey<number, number, List<number>> (k => x => x >= 3 ? consF (k) : ident)
(List.empty)
/**
* `getPropsWith3Gte10 :: Wiki -> Hero -> [Int]`
*
* Returns a list containing all properties where at least 3 spells with at
* least SR 10 belong to.
*/
const getPropsWith3Gte10 =
(staticData: StaticDataRecord) =>
pipe (
HA.spells,
elems,
filterSkillsGte10,
mapByIdKeyMap (SDA.spells (staticData)),
foldr (addSpellToCounter) (OrderedMap.empty),
foldCounter
)
/**
* `getAspectsWith3Gte10 :: Wiki -> Hero -> [Int]`
*
* Returns a list containing all aspects where at least 3 chants with at least
* SR 10 belong to.
*/
const getAspectsWith3Gte10 =
(staticData: StaticDataRecord) =>
pipe (
HA.liturgicalChants,
elems,
filterSkillsGte10,
traceShowId,
mapByIdKeyMap (SDA.liturgicalChants (staticData)),
traceShowId,
foldr (addChantToCounter) (OrderedMap.empty),
traceShowId,
OrderedMap.foldrWithKey ((k: number) => (x: number) => (xs: List<number>) =>
x >= 3 ? consF (k) (xs) : xs)
(List.empty),
traceShowId,
)
const isSocialSkill =
(staticData: StaticDataRecord) =>
pipe (
SOA.id,
ensure (isString),
bindF (lookupF (SDA.skills (staticData))),
fmap (pipe (SA.gr, equals (SkillGroup.Social))),
or
)
const isAddExistSkillSpecAllowed =
(hero: HeroModelRecord) =>
(counter: OrderedMap<string | number, List<string | number>>) =>
(curr_select_id: string | number) =>
pipe_ (
curr_select_id,
ensure (isString),
bindF (lookupF (HA.skills (hero))),
liftM2 ((apps: List<string | number>) =>
(skill: Record<SkillDependent>) =>
flength (apps) < 3 && SkDA.value (skill) >= (flength (apps) + 1) * 6)
(lookupF (counter) (curr_select_id)),
or
)
const isAddNotExistSkillSpecAllowed =
(hero: HeroModelRecord) =>
(selectId: string | number) =>
newensure (selectId, isString)
.bind (id => toNewMaybe (lookup (id) (HA.skills (hero))))
.maybe (false, skill => SkDA.value (skill) >= 6)
const is3or4 = (x: string | number): x is number => x === 3 || x === 4
/**
* Modifies the select options of specific entries to match current conditions.
*/
const modifySelectOptions =
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(hero_magical_traditions: List<Record<ActivatableDependent>>) =>
(wiki_entry: Activatable) =>
(mhero_entry: Maybe<Record<ActivatableDependent>>): ident<Maybe<List<Record<SelectOption>>>> => {
const current_id = AAL.id (wiki_entry)
const isAvailable =
composeT (
filterT (isEntryAvailable (SDA.books (staticData))
(RA.enabledRuleBooks (HA.rules (hero)))
(RA.enableAllRuleBooks (HA.rules (hero)))
(SOA.src)),
filterT (pipe (
SOA.prerequisites,
Maybe.all (reqs => validatePrerequisites (staticData)
(hero)
(reqs)
(current_id))
))
)
const isNoRequiredOrActiveSelection =
composeT (isAvailable, filterT (isNotRequiredNotActive (mhero_entry)))
const isNoRequiredSelection =
composeT (isAvailable, filterT (isNotRequired (mhero_entry)))
switch (current_id) {
case AdvantageId.ExceptionalSkill: {
const hasLessThanTwoSameIdActiveSelections = filterT (areNoSameActive (mhero_entry))
return fmap (filterMapListT (composeT (
isNoRequiredSelection,
hasLessThanTwoSameIdActiveSelections
)))
}
case DisadvantageId.PersonalityFlaw: {
const unique_selections = maybe (List<number> ())
(pipe (
getActiveSelections,
filter (isNumber),
nub
))
(mhero_entry)
const isInfiniteActive: (id: number) => (x: Record<SelectOption>) => boolean =
id => x => SOA.id (x) === id && elem (id) (unique_selections)
const isPrejudiceAndActive = isInfiniteActive (7)
const isUnworldlyAndActive = isInfiniteActive (8)
const isNotActiveAndMaxNotReached: (x: Record<SelectOption>) => boolean =
x => isNotActive (mhero_entry) (x)
&& isNotRequired (mhero_entry) (x)
&& flength (unique_selections) < 2
const filterOptions =
composeT (
isAvailable,
filterT (a => isPrejudiceAndActive (a)
|| isUnworldlyAndActive (a)
|| isNotActiveAndMaxNotReached (a))
)
return fmap (filterMapListT (filterOptions))
}
case DisadvantageId.NegativeTrait:
case DisadvantageId.Maimed:
return fmap (filterMapListT (isNoRequiredOrActiveSelection))
case DisadvantageId.Incompetent: {
const isAdvActive =
pipe (lookupF (HA.advantages (hero)), isMaybeActive)
const isNotSocialSkill = notP (isSocialSkill (staticData))
return fmap (filterMapListT (composeT (
isNoRequiredOrActiveSelection,
filterT (e =>
// Socially Adaptable and Inspire Confidence
// require no Incompetence in social skills
(isAdvActive (AdvantageId.SociallyAdaptable)
|| isAdvActive (AdvantageId.InspireConfidence)
? isNotSocialSkill (e)
: true))
)))
}
case SpecialAbilityId.SkillSpecialization: {
const mcounter = getActiveSecondarySelections (mhero_entry)
return fmap (filterMapListT (composeT (
isNoRequiredSelection,
filterT (e => {
const curr_select_id = SOA.id (e)
// if mcounter is available, mhero_entry must be a Just and thus
// there can be active selections
if (isJust (mcounter)) {
const counter = fromJust (mcounter)
if (member (curr_select_id) (counter)) {
return isAddExistSkillSpecAllowed (hero)
(counter)
(curr_select_id)
}
}
// otherwise we only need to check if the skill rating is at
// least 6, as there can't be an activated selection.
return isAddNotExistSkillSpecAllowed (hero) (curr_select_id)
}),
mapT (e => {
const curr_select_id = SOA.id (e)
const mcounts = bind (mcounter) (lookup (curr_select_id))
const adjustSelectOption =
pipe (
over (SOL.cost)
(isJust (mcounts)
// Increase cost if there are active specializations
// for the same skill
? fmap (multiply (flength (fromJust (mcounts)) + 1))
// otherwise return current cost
: ident),
over (SOL.applications)
(fmap (filter ((app: Record<Application>) => {
const isInactive =
all (notElem<number | string>
(AppA.id (app)))
(mcounts)
const arePrerequisitesMet =
validatePrerequisites (staticData)
(hero)
(maybeToList (AppA.prerequisite (app)))
(current_id)
return isInactive
&& arePrerequisitesMet
})))
)
return adjustSelectOption (e)
})
)))
}
case SpecialAbilityId.TraditionGuildMages: {
return fmap (filterUnfamiliar (pipe (
SpA.tradition,
trads => notElem (MagicalTradition.General) (trads)
&& notElem (MagicalTradition.GuildMages) (trads)
))
(staticData))
}
case SpecialAbilityId.PropertyKnowledge: {
const isValidProperty =
filterT (pipe (SOA.id, elemF<string | number> (getPropsWith3Gte10 (staticData) (hero))))
return fmap (filterMapListT (composeT (
isNoRequiredOrActiveSelection,
isValidProperty
)))
}
case SpecialAbilityId.PropertyFocus: {
const isActivePropertyKnowledge =
filterT (notP (pipe_ (
hero,
HA.specialAbilities,
lookup<string> (SpecialAbilityId.PropertyKnowledge),
isNotActive
)))
return fmap (filterMapListT (composeT (
isNoRequiredOrActiveSelection,
isActivePropertyKnowledge
)))
}
case SpecialAbilityId.AspectKnowledge: {
const valid_aspects = getAspectsWith3Gte10 (staticData) (hero)
traceShowId (valid_aspects)
const isAspectValid = pipe (SOA.id, elemF<string | number> (valid_aspects))
return maybe (ident as ident<Maybe<List<Record<SelectOption>>>>)
((blessed_trad: Record<ActivatableDependent>) =>
fmap (filterMapListT (composeT (
filterT (pipe (
SOA.id,
ensure (isNumber),
bindF (pipe (
getTraditionOfAspect,
mapBlessedNumIdToTradId
)),
Maybe.elem (AAL.id (blessed_trad))
)),
isNoRequiredOrActiveSelection,
filterT (isAspectValid)
))))
(getBlessedTradition (HA.specialAbilities (hero)))
}
case SpecialAbilityId.AdaptionZauber: {
const isWikiEntryFromUnfamiliarTrad =
isUnfamiliarSpell (HA.transferredUnfamiliarSpells (hero))
(hero_magical_traditions)
const isSpellAbove10 =
pipe (
SOA.id,
ensure (isString),
bindF (lookupF (HA.spells (hero))),
maybe (false) (pipe (ASDA.value, gte (10)))
)
const isFromUnfamiliarTrad =
pipe (
SOA.id,
ensure (isString),
bindF (lookupF (SDA.spells (staticData))),
maybe (false) (isWikiEntryFromUnfamiliarTrad)
)
return fmap (filterMapListT (composeT (
isNoRequiredOrActiveSelection,
filterT (isSpellAbove10),
filterT (isFromUnfamiliarTrad)
)))
}
case SpecialAbilityId.SpellEnhancement:
case SpecialAbilityId.ChantEnhancement: {
const getTargetHeroEntry = current_id === SpecialAbilityId.SpellEnhancement
? bindF (lookupF (HA.spells (hero)))
: bindF (lookupF (HA.liturgicalChants (hero)))
const getTargetWikiEntry:
((x: Maybe<string>) => Maybe<Record<Spell> | Record<LiturgicalChant>>) =
current_id === SpecialAbilityId.SpellEnhancement
? bindF (lookupF (SDA.spells (staticData)))
: bindF (lookupF (SDA.liturgicalChants (staticData)))
const isNotUnfamiliar =
(x: Record<Spell> | Record<LiturgicalChant>) =>
LiturgicalChant.is (x)
|| !isUnfamiliarSpell (HA.transferredUnfamiliarSpells (hero))
(hero_magical_traditions)
(x)
return fmap (foldr (isNoRequiredOrActiveSelection (e => {
const mtarget_hero_entry = getTargetHeroEntry (SOA.target (e))
const mtarget_wiki_entry = getTargetWikiEntry (SOA.target (e))
if (
isJust (mtarget_wiki_entry)
&& isJust (mtarget_hero_entry)
&& isNotUnfamiliar (fromJust (mtarget_wiki_entry))
&& ASDA.value (fromJust (mtarget_hero_entry))
>= maybe (0)
(pipe (multiply (4), add (4)))
(SOA.level (e))
) {
const target_wiki_entry = fromJust (mtarget_wiki_entry)
return consF (
set (SOL.name)
(`${SpAL.name (target_wiki_entry)}: ${SOA.name (e)}`)
(e)
)
}
return ident as ident<List<Record<SelectOption>>>
}))
(List ()))
}
case SpecialAbilityId.LanguageSpecializations: {
return pipe_ (
staticData,
SDA.specialAbilities,
lookup<string> (SpecialAbilityId.Language),
bindF (AAL.select),
maybe (cnst (Nothing) as ident<Maybe<List<Record<SelectOption>>>>)
(current_select => {
const available_langs =
// Pair: fst = sid, snd = current_level
maybe (List<number> ())
(pipe (
ADA.active,
foldr ((obj: Record<ActiveObject>) =>
pipe_ (
obj,
AOA.tier,
bindF (current_level =>
pipe_ (
guard (is3or4 (current_level)),
thenF (AOA.sid (obj)),
misNumberM,
fmap ((level: number) => consF (level))
)),
fromMaybe (
ident as ident<List<number>>
)
))
(List ())
))
(pipe_ (
hero,
HA.specialAbilities,
lookup<string> (SpecialAbilityId.Language)
))
const filterLanguages =
foldr (isNoRequiredSelection
(e => {
const lang =
find ((l: number) => l === SOA.id (e))
(available_langs)
// a language must provide either a set of
// possible specializations or an input where a
// custom specialization can be entered.
const provides_specific_or_input =
Maybe.any (notNull) (SOA.specializations (e))
|| isJust (SOA.specializationInput (e))
if (isJust (lang) && provides_specific_or_input) {
return consF (e)
}
return ident as ident<List<Record<SelectOption>>>
}))
(List ())
return cnst (Just (filterLanguages (current_select)))
})
)
}
case SpecialAbilityId.MadaschwesternStil: {
return fmap (filterUnfamiliar (pipe (
SpA.tradition,
trads => notElem (MagicalTradition.General) (trads)
&& notElem (MagicalTradition.Witches) (trads)
))
(staticData))
}
case SpecialAbilityId.ScholarDesMagierkollegsZuHoningen: {
const allowed_traditions = List (
MagicalTradition.Druids,
MagicalTradition.Elves,
MagicalTradition.Witches
)
const mtransferred_spell_trads = pipe_ (
HA.specialAbilities (hero),
lookup<string> (SpecialAbilityId.TraditionGuildMages),
bindF (pipe (ADA.active, listToMaybe)),
bindF (pipe (AOA.sid, isStringM)),
bindF (lookupF (SDA.spells (staticData))),
fmap (SpA.tradition)
)
if (isNothing (mtransferred_spell_trads)) {
return ident
}
const transferred_spell_trads = fromJust (mtransferred_spell_trads)
// Contains all allowed trads the first spell does not have
const trad_diff = filter (notElemF (transferred_spell_trads))
(allowed_traditions)
const has_transferred_all_traditions_allowed = fnull (trad_diff)
return fmap (filterUnfamiliar (pipe (
SpA.tradition,
has_transferred_all_traditions_allowed
? trads =>
notElem (MagicalTradition.General) (trads)
&& List.any (elemF (allowed_traditions)) (trads)
: trads =>
notElem (MagicalTradition.General) (trads)
&& List.any (elemF (trad_diff)) (trads)
))
(staticData))
}
default:
return fmap (filterMapListT (isNoRequiredOrActiveSelection))
}
}
type OtherOptionsModifier = ident<Record<InactiveActivatable>>
const getSermonOrVisionCountMax =
(hero: HeroModelRecord) =>
(adv_id: string) =>
(disadv_id: string) =>
modifyByLevel (3)
(lookup (adv_id) (HA.advantages (hero)))
(lookup (disadv_id) (HA.disadvantages (hero)))
const modifyOtherOptions =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(adventure_points: Record<AdventurePointsCategories>) =>
(wiki_entry: Activatable) =>
(mhero_entry: Maybe<Record<ActivatableDependent>>): Maybe<OtherOptionsModifier> => {
const current_id = AAL.id (wiki_entry)
switch (current_id) {
case DisadvantageId.WenigePredigten:
return pipe_ (
SpecialAbilityGroup.Predigten,
getSermonsAndVisionsCount (wiki) (hero),
getMaxLevelForDecreaseEntry (3),
set (IAL.maxLevel),
Just
)
case DisadvantageId.WenigeVisionen:
return pipe_ (
SpecialAbilityGroup.Visionen,
getSermonsAndVisionsCount (wiki) (hero),
getMaxLevelForDecreaseEntry (3),
set (IAL.maxLevel),
Just
)
case DisadvantageId.SmallSpellSelection: {
return pipe_ (
hero,
countActiveSkillEntries ("spells"),
getMaxLevelForDecreaseEntry (3),
set (IAL.maxLevel),
Just
)
}
case SpecialAbilityId.CraftInstruments: {
return join (liftM2 (
(woodworking: Record<SkillDependent>) =>
(metalworking: Record<SkillDependent>) =>
SkDA.value (woodworking) + SkDA.value (metalworking) >= 12
? Just (ident)
: Nothing
)
(pipe (HA.skills, lookup<string> (SkillId.Woodworking)) (hero))
(pipe (HA.skills, lookup<string> (SkillId.Metalworking)) (hero)))
}
case SpecialAbilityId.Hunter: {
return pipe_ (
CombatTechniqueGroupId.Ranged,
getAllEntriesByGroup (SDA.combatTechniques (wiki))
(HA.combatTechniques (hero)),
filter (pipe (SkDA.value, gte (10))),
flength,
ensure (gt (0)),
mapReplace (ident)
)
}
case SpecialAbilityId.TraditionGuildMages:
case SpecialAbilityId.TraditionWitches:
case SpecialAbilityId.TraditionElves:
case SpecialAbilityId.TraditionDruids:
case SpecialAbilityId.TraditionIllusionist:
case SpecialAbilityId.TraditionQabalyaMage:
case SpecialAbilityId.TraditionGeoden:
case SpecialAbilityId.TraditionZauberalchimisten:
case SpecialAbilityId.TraditionSchelme:
case SpecialAbilityId.TraditionBrobimGeoden: {
return pipe_ (
hero,
HA.specialAbilities,
getMagicalTraditionsHeroEntries,
ensure (List.fnull),
mapReplace (ident)
)
}
case SpecialAbilityId.PropertyKnowledge:
case SpecialAbilityId.AspectKnowledge: {
return pipe_ (
wiki_entry,
AAL.cost,
bindF<number | List<number>, List<number>> (ensure (isList)),
bindF (costs => subscript (costs)
(maybe (0)
(pipe (ADA.active, flength))
(mhero_entry))),
fmap ((cost: number) => pipe_ (cost, Just, set (IAL.cost)))
)
}
case SpecialAbilityId.TraditionChurchOfPraios:
case SpecialAbilityId.TraditionChurchOfRondra:
case SpecialAbilityId.TraditionChurchOfBoron:
case SpecialAbilityId.TraditionChurchOfHesinde:
case SpecialAbilityId.TraditionChurchOfPhex:
case SpecialAbilityId.TraditionChurchOfPeraine:
case SpecialAbilityId.TraditionChurchOfEfferd:
case SpecialAbilityId.TraditionChurchOfTravia:
case SpecialAbilityId.TraditionChurchOfFirun:
case SpecialAbilityId.TraditionChurchOfTsa:
case SpecialAbilityId.TraditionChurchOfIngerimm:
case SpecialAbilityId.TraditionChurchOfRahja:
case SpecialAbilityId.TraditionCultOfTheNamelessOne:
case SpecialAbilityId.TraditionChurchOfAves:
case SpecialAbilityId.TraditionChurchOfIfirn:
case SpecialAbilityId.TraditionChurchOfKor:
case SpecialAbilityId.TraditionChurchOfNandus:
case SpecialAbilityId.TraditionChurchOfSwafnir:
case SpecialAbilityId.TraditionCultOfNuminoru: {
return pipe_ (
hero,
HA.specialAbilities,
getBlessedTradition,
x => isJust (x) ? Nothing : Just (ident)
)
}
case SpecialAbilityId.Recherchegespuer: {
return pipe_ (
hero,
HA.specialAbilities,
lookup<string> (SpecialAbilityId.Wissensdurst),
fmap (ADA.active),
bindF (listToMaybe),
bindF (AOA.sid),
misStringM,
bindF (lookupF (SDA.skills (wiki))),
bindF (skill => pipe (
bindF<number | List<number>, List<number>> (ensure (isList)),
fmap (pipe (
map (add (getAPForActivatation (SA.ic (skill)))),
Just,
set (IAL.cost)
))
)
(AAL.cost (wiki_entry)))
)
}
case SpecialAbilityId.PredigtDerGemeinschaft:
case SpecialAbilityId.PredigtDerZuversicht:
case SpecialAbilityId.PredigtDesGottvertrauens:
case SpecialAbilityId.PredigtDesWohlgefallens:
case SpecialAbilityId.PredigtWiderMissgeschicke: {
const isAdvActive =
pipe (lookupF (HA.advantages (hero)), isMaybeActive)
const max =
getSermonOrVisionCountMax (hero)
(AdvantageId.ZahlreichePredigten)
(DisadvantageId.WenigePredigten)
const isLessThanMax =
countActiveGroupEntries (wiki)
(hero)
(SpecialAbilityGroup.Predigten) < max
return (isAdvActive (AdvantageId.Prediger) && isLessThanMax)
|| isAdvActive (AdvantageId.Blessed)
? Just (ident)
: Nothing
}
case SpecialAbilityId.VisionDerBestimmung:
case SpecialAbilityId.VisionDerEntrueckung:
case SpecialAbilityId.VisionDerGottheit:
case SpecialAbilityId.VisionDesSchicksals:
case SpecialAbilityId.VisionDesWahrenGlaubens: {
const isAdvActive =
pipe (lookupF (HA.advantages (hero)), isMaybeActive)
const max =
getSermonOrVisionCountMax (hero)
(AdvantageId.ZahlreicheVisionen)
(DisadvantageId.WenigeVisionen)
const isLessThanMax =
countActiveGroupEntries (wiki)
(hero)
(SpecialAbilityGroup.Visionen) < max
return (isAdvActive (AdvantageId.Visionaer) && isLessThanMax)
|| isAdvActive (AdvantageId.Blessed)
? Just (ident)
: Nothing
}
case SpecialAbilityId.DunklesAbbildDerBuendnisgabe: {
return pipe_ (
hero,
HA.pact,
fmap (pipe (PA.level, Just, set (IAL.maxLevel)))
)
}
case SpecialAbilityId.TraditionArcaneBard:
case SpecialAbilityId.TraditionArcaneDancer:
case SpecialAbilityId.TraditionIntuitiveMage:
case SpecialAbilityId.TraditionSavant:
case SpecialAbilityId.TraditionAnimisten: {
return APCA.spentOnMagicalAdvantages (adventure_points) <= 25
&& APCA.spentOnMagicalDisadvantages (adventure_points) <= 25
&& pipe_ (
hero,
HA.specialAbilities,
getMagicalTraditionsHeroEntries,
fnull
)
? Just (ident)
: Nothing
}
default:
return Just (ident)
}
}
/**
* Calculates whether an Activatable is valid to add or not and, if valid,
* calculates and filters necessary properties and selection lists. Returns a
* Maybe of the result or `undefined` if invalid.
*/
export const getInactiveView =
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(automatic_advantages: List<string>) =>
(required_apply_to_mag_actions: boolean) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) =>
(adventure_points: Record<AdventurePointsCategories>) =>
(validExtendedSpecialAbilities: List<string>) =>
(hero_magical_traditions: List<Record<ActivatableDependent>>) =>
(wiki_entry: Activatable) =>
(mhero_entry: Maybe<Record<ActivatableDependent>>): Maybe<Record<InactiveActivatable>> => {
const current_id = AAL.id (wiki_entry)
const current_prerequisites = AAL.prerequisites (wiki_entry)
const max_level = isOrderedMap (current_prerequisites)
? validateLevel (staticData)
(hero)
(current_prerequisites)
(maybe<ActivatableDependent["dependencies"]> (List ())
(ADA.dependencies)
(mhero_entry))
(current_id)
: Nothing
const isNotValid = isAdditionDisabled (staticData)
(hero)
(required_apply_to_mag_actions)
(validExtendedSpecialAbilities)
(matchingScriptAndLanguageRelated)
(wiki_entry)
(mhero_entry)
(max_level)
if (!isNotValid) {
return pipe_ (
wiki_entry,
AAL.select,
modifySelectOptions (staticData)
(hero)
(hero_magical_traditions)
(wiki_entry)
(mhero_entry),
ensure (maybe (true) (notNull)),
fmap ((select_options: Maybe<List<Record<SelectOption>>>) => InactiveActivatable ({
id: current_id,
name: SpAL.name (wiki_entry),
cost: AAL.cost (wiki_entry),
maxLevel: max_level,
heroEntry: mhero_entry,
wikiEntry: wiki_entry as Record<RecordI<Activatable>>,
selectOptions: fmapF (select_options)
(sortRecordsByName (staticData)),
isAutomatic: List.elem (AAL.id (wiki_entry))
(automatic_advantages),
})),
ap (modifyOtherOptions (staticData)
(hero)
(adventure_points)
(wiki_entry)
(mhero_entry)),
bindF (ensure (pipe (IAA.maxLevel, maybe (true) (notEquals (0)))))
)
}
return Nothing
}
@@ -1,290 +0,0 @@
/**
* Checks if an `Activatable` entry is available to be activated.
*
* @file src/Utilities/activatableInactiveValidationUtils.ts
* @author Lukas Obermann
* @since 1.1.0
*/
import { not } from "../../../Data/Bool"
import { ident } from "../../../Data/Function"
import { fmap } from "../../../Data/Functor"
import { elem, flength, foldr, isList, List, notElem, toArray } from "../../../Data/List"
import { all, any, bind, bindF, fromMaybe, isJust, listToMaybe, Maybe, sum } from "../../../Data/Maybe"
import { add, inc, lte } from "../../../Data/Num"
import { isOrderedMap, lookup, lookupF } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { SkillGroup, SpecialAbilityGroup } from "../../Constants/Groups"
import { AdvantageId, DisadvantageId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { ActivatableDependency } from "../../Models/Hero/heroTypeHelpers"
import { Pact } from "../../Models/Hero/Pact"
import { Rules } from "../../Models/Hero/Rules"
import { Advantage } from "../../Models/Wiki/Advantage"
import { Skill } from "../../Models/Wiki/Skill"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { Activatable } from "../../Models/Wiki/wikiTypeHelpers"
import { MatchingScriptAndLanguageRelated } from "../../Selectors/activatableSelectors"
import { countActiveGroupEntries, hasActiveGroupEntry } from "../entryGroupUtils"
import { getAllEntriesByGroup } from "../heroStateUtils"
import { ensure, toNewMaybe } from "../Maybe"
import { pipe, pipe_ } from "../pipe"
import { getFirstLevelPrerequisites } from "../Prerequisites/flattenPrerequisites"
import { validatePrerequisites } from "../Prerequisites/validatePrerequisitesUtils"
import { isString } from "../typeCheckUtils"
import * as CheckStyleUtils from "./checkStyleUtils"
import { isActive, isMaybeActive } from "./isActive"
const SDA = StaticData.A
const AA = Advantage.A
const AAL = Advantage.AL
const SAA = SpecialAbility.A
const HA = HeroModel.A
const ADA = ActivatableDependent.A
const AOA = ActiveObject.A
const PA = Pact.A
const RA = Rules.A
const isAdditionDisabledForCombatStyle =
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(wiki_entry: Record<SpecialAbility>): boolean => {
const combination_hero_entry = lookup<string> (SpecialAbilityId.CombatStyleCombination)
(HA.specialAbilities (hero))
// Combination-SA is active, which allows 3 styles to be active,
// but only a maximum of 2 from one type (armed/unarmed).
if (isMaybeActive (combination_hero_entry)) {
const totalActive =
countActiveGroupEntries (staticData)
(hero)
(
SpecialAbilityGroup.CombatStylesArmed,
SpecialAbilityGroup.CombatStylesUnarmed
)
const equalTypeStylesActive =
countActiveGroupEntries (staticData) (hero) (SAA.gr (wiki_entry))
return totalActive >= 3 || equalTypeStylesActive >= 2
}
// Otherwise, only one of each type can be active.
else {
return pipe_ (wiki_entry, SAA.gr, hasActiveGroupEntry (staticData) (hero))
}
}
const isAdditionDisabledSpecialAbilitySpecific =
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
({
languagesWithMatchingScripts,
scriptsWithMatchingLanguages,
}: MatchingScriptAndLanguageRelated) =>
(wiki_entry: Activatable): boolean => {
if (Advantage.is (wiki_entry)) {
switch (AA.id (wiki_entry)) {
case AdvantageId.InspireConfidence: {
return toNewMaybe (lookup<string> (DisadvantageId.Incompetent) (HA.disadvantages (hero)))
.maybe (false, incompetentStateEntry =>
toArray (ADA.active (incompetentStateEntry))
.some (ao =>
toNewMaybe (AOA.sid (ao))
.bind (skillId => ensure (skillId, isString))
.bind (skillId => toNewMaybe (lookupF (SDA.skills (staticData))
(skillId)))
.maybe (
false,
skill => Skill.A.gr (skill) === SkillGroup.Social
)))
}
default:
return false
}
}
else if (SpecialAbility.is (wiki_entry)) {
const current_id = SAA.id (wiki_entry)
if (CheckStyleUtils.isCombatStyleSpecialAbility (wiki_entry)) {
return isAdditionDisabledForCombatStyle (staticData) (hero) (wiki_entry)
}
if (CheckStyleUtils.isMagicalStyleSpecialAbility (wiki_entry)) {
const combination_hero_entry = lookup<string> (SpecialAbilityId.MagicStyleCombination)
(HA.specialAbilities (hero))
const total_active = countActiveGroupEntries (staticData)
(hero)
(SpecialAbilityGroup.MagicalStyles)
return total_active >= (isMaybeActive (combination_hero_entry) ? 2 : 1)
}
if (CheckStyleUtils.isBlessedStyleSpecialAbility (wiki_entry)) {
return hasActiveGroupEntry (staticData) (hero) (SpecialAbilityGroup.BlessedStyles)
}
if (current_id === SpecialAbilityId.CombatStyleCombination) {
return !hasActiveGroupEntry (staticData)
(hero)
(
SpecialAbilityGroup.CombatStylesArmed,
SpecialAbilityGroup.CombatStylesUnarmed
)
}
if (current_id === SpecialAbilityId.MagicStyleCombination) {
return !hasActiveGroupEntry (staticData) (hero) (SpecialAbilityGroup.MagicalStyles)
}
if (current_id === SpecialAbilityId.SpellEnhancement) {
const traditionSchelme = toNewMaybe (lookup<string> (SpecialAbilityId.TraditionSchelme)
(HA.specialAbilities (hero)))
if (traditionSchelme.isJust && isActive (traditionSchelme.value)) {
return true
}
}
if (current_id === SpecialAbilityId.DunklesAbbildDerBuendnisgabe) {
return hasActiveGroupEntry (staticData) (hero) (SpecialAbilityGroup.Paktgeschenke)
}
if (current_id === SpecialAbilityId.WegDerSchreiberin) {
return languagesWithMatchingScripts.length < 1 || scriptsWithMatchingLanguages.length < 1
}
if (SAA.gr (wiki_entry) === SpecialAbilityGroup.Paktgeschenke) {
const dunkles_abbild = lookup<string> (SpecialAbilityId.DunklesAbbildDerBuendnisgabe)
(HA.specialAbilities (hero))
const allPactPresents = getAllEntriesByGroup (SDA.specialAbilities (staticData))
(HA.specialAbilities (hero))
(SpecialAbilityGroup.Paktgeschenke)
const countPactPresents =
foldr ((obj: Record<ActivatableDependent>) => {
if (isActive (obj)) {
const wikiObj = lookup (ADA.id (obj)) (SDA.specialAbilities (staticData))
if (
any (pipe (SAA.prerequisites, isOrderedMap))
(wikiObj)
&& any (pipe (SAA.cost, isList))
(wikiObj)
&& isJust (bind (wikiObj) (SAA.tiers))
) {
return add (sum (
bindF (AOA.tier) (listToMaybe (ADA.active (obj)))
))
}
return inc
}
return ident as ident<number>
})
(0)
(allPactPresents)
// isFaeriePact?
const isDisabled = all (pipe (PA.category, lte (1))) (HA.pact (hero))
? isMaybeActive (dunkles_abbild)
|| all (pipe (PA.level, lte (countPactPresents))) (HA.pact (hero))
// is Lesser Pact?
: all (pipe (PA.level, lte (0))) (HA.pact (hero))
// Lesser Pact only provides 3 PactGifts
? countPactPresents >= 3
// Normal DemonPact: KdV + 7 PactGifts
: all (pipe (PA.level, lte (countPactPresents - 7))) (HA.pact (hero))
return isDisabled
}
if (current_id === SpecialAbilityId.LanguageSpecializations) {
return pipe (HA.rules, RA.enableLanguageSpecializations, not) (hero)
}
if (elem (SAA.gr (wiki_entry)) (List (31, 32))) {
// TODO: add option to activate vampire or lycanthropy and activate this
// SAs based on that option
return true
}
}
return false
}
/**
* Checks if you can somehow add an ActiveObject to the given entry.
* @param state The present state of the current hero.
* @param instance The entry.
*/
const isAdditionDisabledEntrySpecific =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) =>
(wiki_entry: Activatable): boolean =>
isAdditionDisabledSpecialAbilitySpecific (wiki)
(hero)
(matchingScriptAndLanguageRelated)
(wiki_entry)
|| !validatePrerequisites (wiki)
(hero)
(getFirstLevelPrerequisites (AAL.prerequisites (wiki_entry)))
(AAL.id (wiki_entry))
const hasGeneralRestrictionToAdd =
any (pipe (ADA.dependencies, elem<ActivatableDependency> (false)))
const hasReachedMaximumEntries =
(wiki_entry: Activatable) =>
(mhero_entry: Maybe<Record<ActivatableDependent>>) =>
any (lte (fromMaybe (0)
(fmap (pipe (ADA.active, flength))
(mhero_entry))))
(AAL.max (wiki_entry))
const hasReachedImpossibleMaximumLevel = Maybe.elem (0)
const isInvalidExtendedSpecialAbility =
(wiki_entry: Activatable) =>
(validExtendedSpecialAbilities: List<string>) =>
CheckStyleUtils.isExtendedSpecialAbility (wiki_entry)
&& notElem (AAL.id (wiki_entry)) (validExtendedSpecialAbilities)
const doesNotApplyToMagActionsThoughRequired =
(required_apply_to_mag_actions: boolean) =>
(wiki_entry: Activatable): boolean =>
SpecialAbility.is (wiki_entry)
? false
: required_apply_to_mag_actions && Advantage.AL.isExclusiveToArcaneSpellworks (wiki_entry)
/**
* Checks if the given entry can be added.
* @param obj
* @param state The current hero's state.
*/
export const isAdditionDisabled =
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(required_apply_to_mag_actions: boolean) =>
(validExtendedSpecialAbilities: List<string>) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) =>
(wiki_entry: Activatable) =>
(mhero_entry: Maybe<Record<ActivatableDependent>>) =>
(max_level: Maybe<number>): boolean =>
isAdditionDisabledEntrySpecific (wiki) (hero) (matchingScriptAndLanguageRelated) (wiki_entry)
|| hasGeneralRestrictionToAdd (mhero_entry)
|| hasReachedMaximumEntries (wiki_entry) (mhero_entry)
|| hasReachedImpossibleMaximumLevel (max_level)
|| isInvalidExtendedSpecialAbility (wiki_entry) (validExtendedSpecialAbilities)
|| doesNotApplyToMagActionsThoughRequired (required_apply_to_mag_actions) (wiki_entry)
@@ -1,81 +0,0 @@
import { countWith, List } from "../../../Data/List"
import { bindF, listToMaybe, Maybe, maybe, sum } from "../../../Data/Maybe"
import { Record } from "../../../Data/Record"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { pipe } from "../pipe"
import { isMaybeActive } from "./isActive"
const ADA = ActivatableDependent.A
const AOA = ActiveObject.A
const getLevel = pipe (bindF (pipe (ADA.active, listToMaybe)), bindF (AOA.tier), sum)
/**
* ```haskell
* modifyByLevel :: Int -> ActivatableDependent -> ActivatableDependent -> Int
* ```
*
* `modifyByLevel value inc dec` modifies a base `value` by the active level of
* the passed entries. The `inc` entry's level increases the value while the
* `dec` entry's level decreases the value.
*/
export const modifyByLevel =
(value: number) =>
(mincrease: Maybe<Record<ActivatableDependent>>) =>
(mdecrease: Maybe<Record<ActivatableDependent>>): number =>
value + getLevel (mincrease) - getLevel (mdecrease)
/**
* ```haskell
* modifyByLevelM :: Maybe Int -> ActivatableDependent -> ActivatableDependent -> Int
* ```
*
* `modifyByLevel value inc dec` modifies a base `value` by the active level of
* the passed entries. The `inc` entry's level increases the value while the
* `dec` entry's level decreases the value. If `value` is `Nothing`, this
* function always returns `0`.
*/
export const modifyByLevelM =
(mvalue: Maybe<number>) =>
(mincrease: Maybe<Record<ActivatableDependent>>) =>
(mdecrease: Maybe<Record<ActivatableDependent>>): number =>
maybe (0)
((value: number) => modifyByLevel (value) (mincrease) (mdecrease))
(mvalue)
/**
* `getModifierByActiveLevel mbase_mod mincrease mdecrease` adjusts the given
* base `mbase_mod`. If the entry `mincrease`, that should increase the base, is
* active, it adds `1` to the base. If the entry `mdecrease`, that should
* decrease the base, is active, it subtracts `1` from the base. If the
* passed base is `Nothing`, this function always returns `0`.
*/
export const getModifierByIsActive =
(mbase_mod: Maybe<number>) =>
(mincrease: Maybe<Record<ActivatableDependent>>) =>
(mdecrease: Maybe<Record<ActivatableDependent>>): number => {
const hasIncrease = isMaybeActive (mincrease)
const hasDecrease = isMaybeActive (mdecrease)
return maybe (0)
((baseMod: number) =>
hasIncrease ? baseMod + 1 : hasDecrease ? baseMod - 1 : baseMod)
(mbase_mod)
}
/**
* `getModifierByActiveLevels mbase_mod mincreases mdecreases` adjusts the given
* base `mbase_mod`.
*/
export const getModifierByIsActives =
(mbase_mod: Maybe<number>) =>
(mincreases: List<Maybe<Record<ActivatableDependent>>>) =>
(mdecreases: List<Maybe<Record<ActivatableDependent>>>): number => {
const incs = countWith (isMaybeActive) (mincreases)
const decs = countWith (isMaybeActive) (mdecreases)
return maybe (0)
((baseMod: number) => baseMod + incs - decs)
(mbase_mod)
}
@@ -1,494 +0,0 @@
/**
* Generates the complete name of an active `Activatable`. Also provides helper
* functions for compressing `Activatable` name lists and combining or
* extracting parts of the name.
*
* @file src/Utilities/activatableNameUtils.ts
* @author Lukas Obermann
* @since 1.1.0
*/
import { equals } from "../../../Data/Eq"
import { flip, thrush } from "../../../Data/Function"
import { fmap } from "../../../Data/Functor"
import { set } from "../../../Data/Lens"
import { append, appendStr, elem, filter, find, flength, groupByKey, intercalate, List, map, replaceStr, subscriptF } from "../../../Data/List"
import { altF_, any, bind, bindF, catMaybes, elemF, ensure, fromJust, fromMaybe, isJust, isNothing, Just, liftM2, listToMaybe, maybe, Maybe, Nothing, thenF } from "../../../Data/Maybe"
import { elems, lookup, lookupF, OrderedMap, sdelete } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { AdvantageId, AdvantageId as AdvantageIdEnum, DisadvantageId, DisadvantageId as DisadvantageIdEnum, SpecialAbilityId, SpecialAbilityId as SpecialAbilityIdEnum } from "../../Constants/Ids"
import { ActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId"
import { ActivatableCombinedName, ActivatableCombinedNameL } from "../../Models/View/ActivatableCombinedName"
import { ActivatableNameCostSafeCostL } from "../../Models/View/ActivatableNameCost"
import { ActiveActivatable, ActiveActivatableA_, ActiveActivatableL } from "../../Models/View/ActiveActivatable"
import { Advantage } from "../../Models/Wiki/Advantage"
import { Skill } from "../../Models/Wiki/Skill"
import { Application } from "../../Models/Wiki/sub/Application"
import { SelectOption } from "../../Models/Wiki/sub/SelectOption"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { Activatable, ActivatableSkillEntry, EntryWithCategory, SID, SkillishEntry } from "../../Models/Wiki/wikiTypeHelpers"
import { mdash } from "../Chars"
import { composeL } from "../compose"
import { formatList, translate } from "../I18n"
import { ifElse } from "../ifElse"
import { toRoman } from "../NumberUtils"
import { pipe, pipe_ } from "../pipe"
import { sortStrings } from "../sortBy"
import { isNumber, isString, misStringM } from "../typeCheckUtils"
import { getWikiEntry, isActivatableWikiEntry, isSkillishWikiEntry } from "../WikiUtils"
import { findSelectOption, getSelectOptionName } from "./selectionUtils"
const SDA = StaticData.A
const AOWIA = ActiveObjectWithId.A
const AAA_ = ActiveActivatableA_
const AAL = Advantage.AL
const SA = Skill.A
const SAL = Skill.AL
const SOA = SelectOption.A
const AA = Application.A
/**
* Returns the name of the given object. If the object is a string, it returns
* the string.
*/
export const getFullName =
(obj: string | Record<ActiveActivatable>): string => {
if (typeof obj === "string") {
return obj
}
return AAA_.name (obj)
}
/**
* Accepts the full special ability name and returns only the text between
* parentheses. If no parentheses were found, returns an empty string.
*/
export const getBracketedNameFromFullName =
(full_name: string): string => {
const result = /\((?<subname>.+)\)/u .exec (full_name)
if (result === null || result .groups === undefined) {
return ""
}
return result .groups .subname
}
/**
* A lot of entries have customization options: Text input, select option or
* both. This function creates a string that can be appended to the `name`
* property of the respective record to create the full active name.
*/
const getEntrySpecificNameAddition =
(staticData: StaticDataRecord) =>
(wiki_entry: Activatable) =>
(hero_entry: Record<ActiveObjectWithId>): Maybe<string> => {
switch (AOWIA.id (hero_entry)) {
// Entry with Skill selection (string id)
case AdvantageId.Aptitude:
case AdvantageId.ExceptionalSkill:
case AdvantageId.ExceptionalCombatTechnique:
case AdvantageId.WeaponAptitude:
case DisadvantageId.Incompetent:
case SpecialAbilityId.AdaptionZauber:
case SpecialAbilityId.FavoriteSpellwork:
case SpecialAbilityId.Forschungsgebiet:
case SpecialAbilityId.Expertenwissen:
case SpecialAbilityId.Wissensdurst:
case SpecialAbilityId.Recherchegespuer:
case SpecialAbilityId.Lieblingsliturgie:
return pipe (
AOWIA.sid,
misStringM,
bindF (getWikiEntry (staticData)),
bindF<EntryWithCategory, SkillishEntry> (ensure (isSkillishWikiEntry)),
fmap (SAL.name)
)
(hero_entry)
case AdvantageId.HatredOf:
return pipe (
AOWIA.sid,
findSelectOption (wiki_entry),
liftM2 ((type: string | number) => (frequency: Record<SelectOption>) =>
`${type} (${SOA.name (frequency)})`)
(AOWIA.sid2 (hero_entry))
)
(hero_entry)
case DisadvantageId.PersonalityFlaw:
return pipe (
AOWIA.sid,
getSelectOptionName (wiki_entry),
fmap ((option_name: string) =>
maybe (option_name)
// if there is additional input, add to name
((specialInput: string | number) =>
`${option_name}: ${specialInput}`)
(pipe (
AOWIA.sid,
// Check if the select option allows
// additional input
bindF<SID, number> (
ensure (
(x): x is number => isNumber (x)
&& elem (x) (List (7, 8))
)
),
bindF (() => AOWIA.sid2 (hero_entry))
)
(hero_entry)))
)
(hero_entry)
case SpecialAbilityId.SkillSpecialization:
return pipe (
AOWIA.sid,
misStringM,
bindF (lookupF (SDA.skills (staticData))),
bindF (skill => pipe (
AOWIA.sid2,
// If input string use input
misStringM,
// Otherwise lookup application name
altF_ (() => pipe (
SA.applications,
find<Record<Application>> (pipe (
Application.AL.id,
elemF (AOWIA.sid2 (hero_entry))
)),
fmap (AA.name)
)
(skill)),
// Merge skill name and application name
fmap (appl => `${SA.name (skill)}: ${appl}`)
)
(hero_entry))
)
(hero_entry)
case SpecialAbilityId.Exorzist:
return pipe_ (
hero_entry,
AOWIA.tier,
Maybe.product,
ensure (equals (1)),
thenF (AOWIA.sid (hero_entry)),
findSelectOption (wiki_entry),
fmap (SOA.name)
)
case SpecialAbilityId.SpellEnhancement:
case SpecialAbilityId.ChantEnhancement:
return pipe (
AOWIA.sid,
findSelectOption (wiki_entry),
bindF (ext => pipe (
bindF ((target_id: string) => {
const acc =
AOWIA.id (hero_entry)
=== SpecialAbilityId.SpellEnhancement
? SDA.spells
: SDA.liturgicalChants
return lookupF<string, ActivatableSkillEntry>
(acc (staticData))
(target_id)
}),
fmap (
(target_entry: ActivatableSkillEntry) =>
`${SAL.name (target_entry)}: ${SOA.name (ext)}`
)
)
(SOA.target (ext)))
)
(hero_entry)
case SpecialAbilityId.TraditionSavant:
return pipe (
AOWIA.sid,
misStringM,
bindF (lookupF (SDA.skills (staticData))),
fmap (SA.name)
)
(hero_entry)
case SpecialAbilityId.LanguageSpecializations:
return pipe (
SDA.specialAbilities,
lookup<string> (SpecialAbilityId.Language),
bindF (pipe (
findSelectOption,
thrush (AOWIA.sid (hero_entry))
)),
bindF (lang => pipe (
AOWIA.sid2,
bindF (
ifElse<string | number, string>
(isString)
<Maybe<string>>
(Just)
(spec_id => bind (SOA.specializations (lang))
(subscriptF (spec_id - 1)))
),
fmap (spec => `${SOA.name (lang)}: ${spec}`)
)
(hero_entry))
)
(staticData)
case SpecialAbilityId.Fachwissen: {
const getApp = (getSid: (r: Record<ActiveObjectWithId>) => Maybe<string | number>) =>
pipe (
SA.applications,
filter (pipe (AA.prerequisite, isNothing)),
find (pipe (AA.id, elemF (getSid (hero_entry)))),
fmap (AA.name)
)
return pipe_ (
hero_entry,
AOWIA.sid,
misStringM,
bindF (lookupF (SDA.skills (staticData))),
bindF (skill => pipe_ (
List (
getApp (AOWIA.sid2) (skill),
getApp (AOWIA.sid3) (skill)
),
catMaybes,
ensure (xs => flength (xs) === 2),
fmap (pipe (
sortStrings (staticData),
formatList ("conjunction") (staticData),
apps => `${SA.name (skill)}: ${apps}`
))
))
)
}
case SpecialAbilityId.Handwerkskunst:
case SpecialAbilityId.KindDerNatur:
case SpecialAbilityId.KoerperlichesGeschick:
case SpecialAbilityId.SozialeKompetenz:
case SpecialAbilityId.Universalgenie: {
return pipe_ (
List (AOWIA.sid (hero_entry), AOWIA.sid2 (hero_entry), AOWIA.sid3 (hero_entry)),
map (getSelectOptionName (wiki_entry)),
catMaybes,
sortStrings (staticData),
intercalate (", "),
Just
)
}
default: {
const current_sid = AOWIA.sid (hero_entry)
const current_sid2 = AOWIA.sid2 (hero_entry)
// Text input
if (isJust (AAL.input (wiki_entry)) && any (isString) (current_sid)) {
return current_sid
}
// Select option and text input
if (isJust (AAL.select (wiki_entry))
&& isJust (AAL.input (wiki_entry))
&& isJust (current_sid)
&& any (isString) (current_sid2)) {
const name1 = fromMaybe ("") (getSelectOptionName (wiki_entry) (current_sid))
const name2 = fromJust (current_sid2)
return Just (`${name1}: ${name2}`)
}
// Plain select option
if (isJust (AAL.select (wiki_entry))) {
return getSelectOptionName (wiki_entry) (current_sid)
}
return Nothing
}
}
}
const addSndinParenthesis = (snd: string) => replaceStr (")") (`: ${snd})`)
/**
* Some entries cannot use the default `name` property from wiki entries. The
* value returned by may not use the default `name` property. For all entries
* that do not need to handle a specific display format, the default `name`
* property is used.
*/
const getEntrySpecificNameReplacements =
(staticData: StaticDataRecord) =>
(wiki_entry: Activatable) =>
(hero_entry: Record<ActiveObjectWithId>) =>
(mname_add: Maybe<string>): string => {
const def = fromMaybe (AAL.name (wiki_entry))
const maybeMap = (f: (x: string) => string) => maybe (AAL.name (wiki_entry))
(f)
(mname_add)
switch (AAL.id (wiki_entry)) {
case AdvantageId.ImmunityToPoison:
case AdvantageId.ImmunityToDisease:
return maybeMap (
name_add => `${translate (staticData) ("advantagesdisadvantages.immunityto")} ${name_add}`
)
case AdvantageId.HatredOf:
return maybeMap (
name_add => `${translate (staticData) ("advantagesdisadvantages.hatredfor")} ${name_add}`
)
case DisadvantageId.AfraidOf:
return maybeMap (
name_add => `${translate (staticData) ("advantagesdisadvantages.afraidof")} ${name_add}`
)
case DisadvantageId.Principles:
case DisadvantageId.Obligations:
return def (liftM2 ((level: number) => (name_add: string) =>
`${AAL.name (wiki_entry)} ${toRoman (level)} (${name_add})`)
(AOWIA.tier (hero_entry))
(mname_add))
case SpecialAbilityId.GebieterDesAspekts:
return maybeMap (name_add => `${AAL.name (wiki_entry)} ${name_add}`)
case SpecialAbilityId.TraditionArcaneBard:
case SpecialAbilityId.TraditionArcaneDancer:
case SpecialAbilityId.TraditionSavant: {
return maybeMap (flip (addSndinParenthesis) (AAL.name (wiki_entry)))
}
default:
return maybeMap (name_add => `${AAL.name (wiki_entry)} (${name_add})`)
}
}
/**
* Returns name, splitted and combined, of advantage/disadvantage/special
* ability as a Maybe (in case the wiki entry does not exist).
* @param instance The ActiveObject with origin id.
* @param wiki The current hero's state.
* @param l10n The locale-dependent messages.
*/
export const getName =
(staticData: StaticDataRecord) =>
(hero_entry: Record<ActiveObjectWithId>): Maybe<Record<ActivatableCombinedName>> =>
pipe (
AOWIA.id,
getWikiEntry (staticData),
bindF<EntryWithCategory, Activatable> (ensure (isActivatableWikiEntry)),
fmap ((wiki_entry: Activatable) => {
const maddName = getEntrySpecificNameAddition (staticData)
(wiki_entry)
(hero_entry)
const fullName = getEntrySpecificNameReplacements (staticData)
(wiki_entry)
(hero_entry)
(maddName)
return ActivatableCombinedName ({
name: fullName,
baseName: AAL.name (wiki_entry),
addName: maddName,
})
})
)
(hero_entry)
const extractCustom = (
id: string,
groupedEntries: OrderedMap<string, List<Record<ActiveActivatable>>>
) =>
pipe_ (
groupedEntries,
lookup (id),
maybe (List<List<Record<ActiveActivatable>>> ())
(map (active => List (set (composeL (
ActiveActivatableL.nameAndCost,
ActivatableNameCostSafeCostL.naming,
ActivatableCombinedNameL.name
))
(fromMaybe (mdash) (AAA_.addName (active)))
(active))))
)
/**
* `compressList :: L10n -> [ActiveActivatable] -> String`
*
* Takes a list of active Activatables and merges them together. Used to display
* lists of Activatables on character sheet.
*/
export const compressList =
(staticData: StaticDataRecord) =>
(xs: List<Record<ActiveActivatable>>): string => {
const groupedEntries = groupByKey<Record<ActiveActivatable>, string> (AAA_.id) (xs)
const customAdvantages =
extractCustom (AdvantageIdEnum.CustomAdvantage, groupedEntries)
const customDisadvantages =
extractCustom (DisadvantageIdEnum.CustomDisadvantage, groupedEntries)
const customSpecialAbilities =
extractCustom (SpecialAbilityIdEnum.CustomSpecialAbility, groupedEntries)
const grouped_xs =
pipe_ (
groupedEntries,
sdelete <string> (AdvantageIdEnum.CustomAdvantage),
sdelete <string> (DisadvantageIdEnum.CustomDisadvantage),
sdelete <string> (SpecialAbilityIdEnum.CustomSpecialAbility),
elems,
append (customAdvantages),
append (customDisadvantages),
append (customSpecialAbilities),
)
return pipe (
map (
ifElse<List<Record<ActiveActivatable>>>
(xs_group => flength (xs_group) === 1)
(pipe (listToMaybe, maybe ("") (AAA_.name)))
(xs_group => pipe (
map ((x: Record<ActiveActivatable>) => {
const levelPart =
pipe (
AAA_.level,
fmap (pipe (toRoman, appendStr (" "))),
fromMaybe ("")
)
(x)
const selectOptionPart =
fromMaybe ("") (AAA_.addName (x))
return selectOptionPart + levelPart
}),
sortStrings (staticData),
intercalate (", "),
x => ` (${x})`,
x => maybe ("")
((r: Record<ActiveActivatable>) =>
AAA_.baseName (r) + x)
(listToMaybe (xs_group))
)
(xs_group))
),
sortStrings (staticData),
intercalate (", ")
)
(grouped_xs)
}
-24
View File
@@ -1,24 +0,0 @@
import { notNull } from "../../../Data/List"
import { Just, Maybe, maybe } from "../../../Data/Maybe"
import { Record } from "../../../Data/Record"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { pipe } from "../pipe"
const { active } = ActivatableDependent.AL
/**
* Checks if the entry is active. This will be the case if there is at least one
* `ActiveObject` in the `obj.active` array.
* @param obj The entry.
*/
export const isActive: (obj: Record<ActivatableDependent>) => boolean =
pipe (active, notNull)
/**
* Checks if the entry is active. This will be the case if there is at least one
* `ActiveObject` in the `obj.active` array.
* @param obj The entry.
*/
export const isMaybeActive =
(obj: Maybe<Record<ActivatableDependent>>): obj is Just<Record<ActivatableDependent>> =>
maybe (false) (isActive) (obj)
@@ -1,383 +0,0 @@
import { filter, List } from "../../../Data/List"
import { bindF, Just, mapMaybe, Maybe, Nothing } from "../../../Data/Maybe"
import { elems, find, lookupF, OrderedMap } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { BlessedTradition, MagicalTradition } from "../../Constants/Groups"
import { SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { pipe } from "../pipe"
import { isActive } from "./isActive"
const ADA = ActivatableDependent.A
/**
* Checks if an ID is a magical tradition's id.
*/
export const isMagicalTradId =
(tradition_id: string): boolean => {
switch (tradition_id) {
case SpecialAbilityId.TraditionGuildMages:
case SpecialAbilityId.TraditionWitches:
case SpecialAbilityId.TraditionElves:
case SpecialAbilityId.TraditionDruids:
case SpecialAbilityId.TraditionIllusionist:
case SpecialAbilityId.TraditionArcaneBard:
case SpecialAbilityId.TraditionArcaneDancer:
case SpecialAbilityId.TraditionIntuitiveMage:
case SpecialAbilityId.TraditionSavant:
case SpecialAbilityId.TraditionQabalyaMage:
case SpecialAbilityId.TraditionZauberalchimisten:
case SpecialAbilityId.TraditionKristallomanten:
case SpecialAbilityId.TraditionGeoden:
case SpecialAbilityId.TraditionSchelme:
case SpecialAbilityId.TraditionAnimisten:
case SpecialAbilityId.TraditionZibilijas:
case SpecialAbilityId.TraditionBrobimGeoden:
return true
default:
return false
}
}
/**
* Checks if an ID is a blessed tradition's id.
*/
export const isBlessedTradId =
(tradition_id: string): boolean => {
switch (tradition_id) {
case SpecialAbilityId.TraditionChurchOfPraios:
case SpecialAbilityId.TraditionChurchOfRondra:
case SpecialAbilityId.TraditionChurchOfBoron:
case SpecialAbilityId.TraditionChurchOfHesinde:
case SpecialAbilityId.TraditionChurchOfPhex:
case SpecialAbilityId.TraditionChurchOfPeraine:
case SpecialAbilityId.TraditionChurchOfEfferd:
case SpecialAbilityId.TraditionChurchOfTravia:
case SpecialAbilityId.TraditionChurchOfFirun:
case SpecialAbilityId.TraditionChurchOfTsa:
case SpecialAbilityId.TraditionChurchOfIngerimm:
case SpecialAbilityId.TraditionChurchOfRahja:
case SpecialAbilityId.TraditionCultOfTheNamelessOne:
case SpecialAbilityId.TraditionChurchOfAves:
case SpecialAbilityId.TraditionChurchOfIfirn:
case SpecialAbilityId.TraditionChurchOfKor:
case SpecialAbilityId.TraditionChurchOfNandus:
case SpecialAbilityId.TraditionChurchOfSwafnir:
case SpecialAbilityId.TraditionCultOfNuminoru:
case SpecialAbilityId.TraditionCultOfLevthan:
return true
default:
return false
}
}
const isActiveMagicalTradition =
(e: Record<ActivatableDependent>) => isMagicalTradId (ADA.id (e)) && isActive (e)
const isActiveBlessedTradition =
(e: Record<ActivatableDependent>) => isBlessedTradId (ADA.id (e)) && isActive (e)
/**
* Get magical traditions' dependent entries.
* @param list
*/
export const getMagicalTraditionsHeroEntries =
pipe (
elems as
(list: OrderedMap<any, Record<ActivatableDependent>>) => List<Record<ActivatableDependent>>,
filter (isActiveMagicalTradition)
)
/**
* Get magical traditions' wiki entries.
* @param wiki
* @param list
*/
export const getMagicalTraditionsFromWiki =
(wiki: OrderedMap<string, Record<SpecialAbility>>) =>
pipe (getMagicalTraditionsHeroEntries, mapMaybe (pipe (ADA.id, lookupF (wiki))))
/**
* Get blessed traditions' dependent entry.
* @param list
*/
export const getBlessedTradition = find (isActiveBlessedTradition)
/**
* Get blessed tradition's' wiki entry.
* @param wiki
* @param list
*/
export const getBlessedTraditionFromWiki =
(wiki: OrderedMap<string, Record<SpecialAbility>>) =>
pipe (getBlessedTradition, bindF (pipe (ADA.id, lookupF (wiki))))
/**
* Map a magical tradition's ID to a numeric magical tradition ID used by
* spells.
*
* *Note: Zauberalchimisten do not get spells and thus do not have a numeric
* magical tradition ID.*
*/
export const mapMagicalTradIdToNumId =
(tradition_id: string): Maybe<MagicalTradition> => {
switch (tradition_id) {
case SpecialAbilityId.TraditionGuildMages:
return Just (MagicalTradition.GuildMages)
case SpecialAbilityId.TraditionWitches:
return Just (MagicalTradition.Witches)
case SpecialAbilityId.TraditionElves:
return Just (MagicalTradition.Elves)
case SpecialAbilityId.TraditionDruids:
return Just (MagicalTradition.Druids)
case SpecialAbilityId.TraditionIllusionist:
return Just (MagicalTradition.Scharlatane)
case SpecialAbilityId.TraditionArcaneBard:
return Just (MagicalTradition.ArcaneBards)
case SpecialAbilityId.TraditionArcaneDancer:
return Just (MagicalTradition.ArcaneDancers)
case SpecialAbilityId.TraditionIntuitiveMage:
return Just (MagicalTradition.IntuitiveZauberer)
case SpecialAbilityId.TraditionSavant:
return Just (MagicalTradition.Meistertalentierte)
case SpecialAbilityId.TraditionQabalyaMage:
return Just (MagicalTradition.Qabalyamagier)
case SpecialAbilityId.TraditionKristallomanten:
return Just (MagicalTradition.Kristallomanten)
case SpecialAbilityId.TraditionGeoden:
return Just (MagicalTradition.Geodes)
case SpecialAbilityId.TraditionSchelme:
return Just (MagicalTradition.Rogues)
case SpecialAbilityId.TraditionAnimisten:
return Just (MagicalTradition.Animists)
case SpecialAbilityId.TraditionZibilijas:
return Just (MagicalTradition.Zibilija)
case SpecialAbilityId.TraditionBrobimGeoden:
return Just (MagicalTradition.BrobimGeoden)
default:
return Nothing
}
}
/**
* Map a numeric magical tradition ID used by spells to a magical tradition's
* ID.
*/
export const mapMagicalNumIdToTradId =
(tradition_id: MagicalTradition): Maybe<SpecialAbilityId> => {
switch (tradition_id) {
case MagicalTradition.GuildMages:
return Just (SpecialAbilityId.TraditionGuildMages)
case MagicalTradition.Witches:
return Just (SpecialAbilityId.TraditionWitches)
case MagicalTradition.Elves:
return Just (SpecialAbilityId.TraditionElves)
case MagicalTradition.Druids:
return Just (SpecialAbilityId.TraditionDruids)
case MagicalTradition.Scharlatane:
return Just (SpecialAbilityId.TraditionIllusionist)
case MagicalTradition.ArcaneBards:
return Just (SpecialAbilityId.TraditionArcaneBard)
case MagicalTradition.ArcaneDancers:
return Just (SpecialAbilityId.TraditionArcaneDancer)
case MagicalTradition.IntuitiveZauberer:
return Just (SpecialAbilityId.TraditionIntuitiveMage)
case MagicalTradition.Meistertalentierte:
return Just (SpecialAbilityId.TraditionSavant)
case MagicalTradition.Qabalyamagier:
return Just (SpecialAbilityId.TraditionQabalyaMage)
case MagicalTradition.Kristallomanten:
return Just (SpecialAbilityId.TraditionKristallomanten)
case MagicalTradition.Geodes:
return Just (SpecialAbilityId.TraditionGeoden)
case MagicalTradition.Rogues:
return Just (SpecialAbilityId.TraditionSchelme)
case MagicalTradition.Animists:
return Just (SpecialAbilityId.TraditionAnimisten)
case MagicalTradition.Zibilija:
return Just (SpecialAbilityId.TraditionZibilijas)
case MagicalTradition.BrobimGeoden:
return Just (SpecialAbilityId.TraditionBrobimGeoden)
default:
return Nothing
}
}
/**
* Map a blessed tradition's ID to a numeric blessed tradition ID used by
* chants.
*/
export const mapBlessedTradIdToNumId =
(tradition_id: string): Maybe<BlessedTradition> => {
switch (tradition_id) {
case SpecialAbilityId.TraditionChurchOfPraios:
return Just (BlessedTradition.ChurchOfPraios)
case SpecialAbilityId.TraditionChurchOfRondra:
return Just (BlessedTradition.ChurchOfRondra)
case SpecialAbilityId.TraditionChurchOfBoron:
return Just (BlessedTradition.ChurchOfBoron)
case SpecialAbilityId.TraditionChurchOfHesinde:
return Just (BlessedTradition.ChurchOfHesinde)
case SpecialAbilityId.TraditionChurchOfPhex:
return Just (BlessedTradition.ChurchOfPhex)
case SpecialAbilityId.TraditionChurchOfPeraine:
return Just (BlessedTradition.ChurchOfPeraine)
case SpecialAbilityId.TraditionChurchOfEfferd:
return Just (BlessedTradition.ChurchOfEfferd)
case SpecialAbilityId.TraditionChurchOfTravia:
return Just (BlessedTradition.ChurchOfTravia)
case SpecialAbilityId.TraditionChurchOfFirun:
return Just (BlessedTradition.ChurchOfFirun)
case SpecialAbilityId.TraditionChurchOfTsa:
return Just (BlessedTradition.ChurchOfTsa)
case SpecialAbilityId.TraditionChurchOfIngerimm:
return Just (BlessedTradition.ChurchOfIngerimm)
case SpecialAbilityId.TraditionChurchOfRahja:
return Just (BlessedTradition.ChurchOfRahja)
case SpecialAbilityId.TraditionCultOfTheNamelessOne:
return Just (BlessedTradition.CultOfTheNamelessOne)
case SpecialAbilityId.TraditionChurchOfAves:
return Just (BlessedTradition.ChurchOfAves)
case SpecialAbilityId.TraditionChurchOfIfirn:
return Just (BlessedTradition.ChurchOfIfirn)
case SpecialAbilityId.TraditionChurchOfKor:
return Just (BlessedTradition.ChurchOfKor)
case SpecialAbilityId.TraditionChurchOfNandus:
return Just (BlessedTradition.ChurchOfNandus)
case SpecialAbilityId.TraditionChurchOfSwafnir:
return Just (BlessedTradition.ChurchOfSwafnir)
case SpecialAbilityId.TraditionCultOfNuminoru:
return Just (BlessedTradition.CultOfNuminoru)
case SpecialAbilityId.TraditionCultOfLevthan:
return Just (BlessedTradition.Levthankult)
default:
return Nothing
}
}
/**
* Map a blessed tradition's ID to a numeric blessed tradition ID used by
* chants.
*/
export const mapBlessedNumIdToTradId =
(tradition_id: BlessedTradition): Maybe<SpecialAbilityId> => {
switch (tradition_id) {
case BlessedTradition.ChurchOfPraios:
return Just (SpecialAbilityId.TraditionChurchOfPraios)
case BlessedTradition.ChurchOfRondra:
return Just (SpecialAbilityId.TraditionChurchOfRondra)
case BlessedTradition.ChurchOfBoron:
return Just (SpecialAbilityId.TraditionChurchOfBoron)
case BlessedTradition.ChurchOfHesinde:
return Just (SpecialAbilityId.TraditionChurchOfHesinde)
case BlessedTradition.ChurchOfPhex:
return Just (SpecialAbilityId.TraditionChurchOfPhex)
case BlessedTradition.ChurchOfPeraine:
return Just (SpecialAbilityId.TraditionChurchOfPeraine)
case BlessedTradition.ChurchOfEfferd:
return Just (SpecialAbilityId.TraditionChurchOfEfferd)
case BlessedTradition.ChurchOfTravia:
return Just (SpecialAbilityId.TraditionChurchOfTravia)
case BlessedTradition.ChurchOfFirun:
return Just (SpecialAbilityId.TraditionChurchOfFirun)
case BlessedTradition.ChurchOfTsa:
return Just (SpecialAbilityId.TraditionChurchOfTsa)
case BlessedTradition.ChurchOfIngerimm:
return Just (SpecialAbilityId.TraditionChurchOfIngerimm)
case BlessedTradition.ChurchOfRahja:
return Just (SpecialAbilityId.TraditionChurchOfRahja)
case BlessedTradition.CultOfTheNamelessOne:
return Just (SpecialAbilityId.TraditionCultOfTheNamelessOne)
case BlessedTradition.ChurchOfAves:
return Just (SpecialAbilityId.TraditionChurchOfAves)
case BlessedTradition.ChurchOfIfirn:
return Just (SpecialAbilityId.TraditionChurchOfIfirn)
case BlessedTradition.ChurchOfKor:
return Just (SpecialAbilityId.TraditionChurchOfKor)
case BlessedTradition.ChurchOfNandus:
return Just (SpecialAbilityId.TraditionChurchOfNandus)
case BlessedTradition.ChurchOfSwafnir:
return Just (SpecialAbilityId.TraditionChurchOfSwafnir)
case BlessedTradition.CultOfNuminoru:
return Just (SpecialAbilityId.TraditionCultOfNuminoru)
case BlessedTradition.Levthankult:
return Just (SpecialAbilityId.TraditionCultOfLevthan)
default:
return Nothing
}
}
@@ -1,596 +0,0 @@
/**
* Calculate the Adventure Points for when removing the entry. This might not
* be the actual cost. In those cases the AP difference needs to be calculated
* for AP spent total.
*
* @file src/Utilities/activatableCostUtils.ts
* @author Lukas Obermann
* @since 1.1.0
*/
import { cnst, flip, ident } from "../../../Data/Function"
import { fmap, fmapF } from "../../../Data/Functor"
import { over, set } from "../../../Data/Lens"
import { appendStr, countWith, filter, foldl, ifoldr, isList, List, map, notElem, notNull, subscript, subscriptF, sum } from "../../../Data/List"
import { any, bind, bindF, elem, elemF, ensure, fromJust, fromMaybe, isJust, isNothing, joinMaybeList, Just, liftM2, listToMaybe, mapMaybe, Maybe, maybe, Nothing } from "../../../Data/Maybe"
import { add, gt, multiply, negate } from "../../../Data/Num"
import { lookup, lookupF } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { Pair } from "../../../Data/Tuple"
import { Category } from "../../Constants/Categories"
import { AdvantageId, DisadvantageId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDependent, isActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { ActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { ActivatableNameCost, ActivatableNameCostA_, ActivatableNameCostL, ActivatableNameCostL_, ActivatableNameCostSafeCost } from "../../Models/View/ActivatableNameCost"
import { Advantage } from "../../Models/Wiki/Advantage"
import { Disadvantage, isDisadvantage } from "../../Models/Wiki/Disadvantage"
import { Skill } from "../../Models/Wiki/Skill"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { Activatable, EntryWithCategory, SkillishEntry } from "../../Models/Wiki/wikiTypeHelpers"
import { isMaybeActive } from "../Activatable/isActive"
import { getSelectOptionCost } from "../Activatable/selectionUtils"
import { nbsp, nobr } from "../Chars"
import { getHeroStateItem } from "../heroStateUtils"
import { translate } from "../I18n"
import { getCategoryById } from "../IDUtils"
import { getAPForActivatation, icToIx } from "../ImprovementCost"
import { toRoman } from "../NumberUtils"
import { pipe, pipe_ } from "../pipe"
import { isNumber, misNumberM, misStringM } from "../typeCheckUtils"
import { getWikiEntry, isActivatableWikiEntry, isSkillishWikiEntry } from "../WikiUtils"
const HA = HeroModel.A
const AAL = Advantage.AL
const AOWIA = ActiveObjectWithId.A
const isDisadvantageActive =
(id: string) =>
pipe (
HA.disadvantages,
lookup (id),
isMaybeActive
)
const getCostForEntryWithSkillSel =
(ensureId: (x: Maybe<number | string>) => Maybe<string>) =>
(wiki: StaticDataRecord) =>
(mcurrent_sid: Maybe<string | number>) =>
(mcurrent_cost: Maybe<number | List<number>>) =>
pipe_ (
mcurrent_sid,
ensureId,
bindF (getWikiEntry (wiki)),
bindF<EntryWithCategory, SkillishEntry> (ensure (isSkillishWikiEntry)),
bindF (skill => pipe_ (
mcurrent_cost,
bindF (ensure (isList)),
// Use the IC as an index for the list
// of AP
bindF (subscriptF (icToIx (Skill.AL.ic (skill))))
))
)
const isPersonalityFlawNotPaid =
(sid: number) =>
(paid_entries_max: number) =>
(isEntryToAdd: boolean) =>
(all_active: List<Record<ActiveObject>>) =>
(mcurrent_sid: Maybe<string | number>) =>
elemF (mcurrent_sid) (sid)
&& countWith ((e: Record<ActiveObject>) =>
pipe (ActiveObject.AL.sid, elem<string | number> (sid)) (e)
// Entries with custom cost are ignored for the rule
&& isNothing (ActiveObject.AL.cost (e)))
(all_active) > (isEntryToAdd ? paid_entries_max - 1 : paid_entries_max)
/**
* A function for folding over a list of `ActiveObject`s to get the highest
* level. Ignores entries with custom cost.
*
* `foldl compareMaxLevel 0 all_entries`
*/
export const compareMaxLevel =
(previous_max: number) =>
(active: Record<ActiveObject>) => {
const mactive_level = ActiveObject.AL.tier (active)
if (isJust (mactive_level)) {
const active_level = fromJust (mactive_level)
return active_level > previous_max
&& isNothing (ActiveObject.AL.cost (active))
? active_level
: previous_max
}
return previous_max
}
/**
* A function for folding over a list of `ActiveObject`s to get the
* second-highest level. Ignores entries with custom cost.
*
* `foldl (compareSubMaxLevel max_level) 0 all_entries`
*/
export const compareSubMaxLevel =
(max: number) =>
(previous_max: number) =>
(active: Record<ActiveObject>) => {
const mactive_level = ActiveObject.AL.tier (active)
if (isJust (mactive_level)) {
const active_level = fromJust (mactive_level)
return active_level > previous_max
&& active_level < max
&& isNothing (ActiveObject.AL.cost (active))
? active_level
: previous_max
}
return previous_max
}
/**
* Returns the value(s) how the spent AP value would change after removing the
* respective entry.
*
* @param isEntryToAdd If `entry` has not been added to the list of active
* entries yet, this must be `true`, otherwise `false`.
*/
const getEntrySpecificCost =
(isEntryToAdd: boolean) =>
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(wiki_entry: Activatable) =>
(hero_entry: Maybe<Record<ActivatableDependent>>) =>
// tslint:disable-next-line: cyclomatic-complexity
(entry: Record<ActiveObjectWithId>): Maybe<number | List<number>> => {
const current_id = AOWIA.id (entry)
const mcurrent_sid = AOWIA.sid (entry)
const mcurrent_sid2 = AOWIA.sid2 (entry)
const mcurrent_sid3 = AOWIA.sid3 (entry)
const mcurrent_level = AOWIA.tier (entry)
const mcurrent_cost = AAL.cost (wiki_entry)
const all_active = joinMaybeList (fmap (ActivatableDependent.A.active) (hero_entry))
switch (current_id) {
// Entry with Skill selection
case AdvantageId.Aptitude:
case AdvantageId.ExceptionalSkill:
case AdvantageId.ExceptionalCombatTechnique:
case AdvantageId.WeaponAptitude:
case DisadvantageId.Incompetent:
case SpecialAbilityId.AdaptionZauber:
case SpecialAbilityId.FavoriteSpellwork:
case SpecialAbilityId.Forschungsgebiet:
case SpecialAbilityId.Expertenwissen:
case SpecialAbilityId.Wissensdurst:
case SpecialAbilityId.Lieblingsliturgie:
case SpecialAbilityId.WegDerGelehrten:
case SpecialAbilityId.WegDerKuenstlerin:
case SpecialAbilityId.Fachwissen: {
return getCostForEntryWithSkillSel (misStringM)
(wiki)
(mcurrent_sid)
(mcurrent_cost)
}
case DisadvantageId.PersonalityFlaw: {
if (
// 7 = "Prejudice" => more than one entry possible
// more than one entry of Prejudice does not contribute to AP spent
isPersonalityFlawNotPaid (7) (1) (isEntryToAdd) (all_active) (mcurrent_sid)
// 8 = "Unworldly" => more than one entry possible
// more than two entries of Unworldly do not contribute to AP spent
|| isPersonalityFlawNotPaid (8) (2) (isEntryToAdd) (all_active) (mcurrent_sid)
) {
return Just (0)
}
return getSelectOptionCost (wiki_entry) (mcurrent_sid)
}
case DisadvantageId.Principles:
case DisadvantageId.Obligations: {
const current_max_level = foldl (compareMaxLevel)
(0)
(all_active)
const current_second_max_level = foldl (compareSubMaxLevel (current_max_level))
(0)
(all_active)
if (isNothing (mcurrent_level)) {
return Nothing
}
const current_level = fromJust (mcurrent_level)
if (
// If the entry is not the one with the highest level, adding or
// removing it won't affect AP spent at all
current_max_level > current_level
// If there is more than one entry on the same level if this entry is
// active, it won't affect AP spent at all. Thus, if the entry is to
// be added, there must be at least one (> 0) entry for this rule to
// take effect.
|| countWith (pipe (ActiveObject.AL.tier, elem (current_level)))
(all_active) > (isEntryToAdd ? 0 : 1)
) {
return Nothing
}
// Otherwise, the level difference results in the cost.
return fmapF (misNumberM (mcurrent_cost))
(multiply (current_level - current_second_max_level))
}
case DisadvantageId.BadHabit: {
// more than three entries cannot contribute to AP spent; entries with
// custom cost are ignored for the rule's effect
if (countWith (pipe (ActiveObject.AL.cost, isNothing))
(all_active) > (isEntryToAdd ? 2 : 3)) {
return Nothing
}
return mcurrent_cost
}
case SpecialAbilityId.SkillSpecialization: {
return pipe_ (
mcurrent_sid,
misStringM,
bindF (
current_sid =>
fmapF (lookup (current_sid)
(StaticData.A.skills (wiki)))
(skill =>
// Multiply number of final occurences of the
// same skill...
(countWith ((e: Record<ActiveObject>) =>
pipe (
ActiveObject.AL.sid,
elem<string | number> (current_sid)
)
(e)
// Entries with custom cost are ignored for the rule
&& isNothing (ActiveObject.AL.cost (e)))
(all_active) + (isEntryToAdd ? 1 : 0))
// ...with the skill's IC
* getAPForActivatation (Skill.AL.ic (skill)))
)
)
}
case SpecialAbilityId.Language: {
// Native Tongue (level 4) does not cost anything
return elem (4) (mcurrent_level) ? Nothing : mcurrent_cost
}
case SpecialAbilityId.PropertyKnowledge:
case SpecialAbilityId.AspectKnowledge: {
// Does not include custom cost activations in terms of calculated cost
const amount = countWith (pipe (ActiveObject.AL.cost, isNothing))
(all_active)
const index = amount + (isEntryToAdd ? 0 : -1)
if (isNothing (mcurrent_cost)) {
return Nothing
}
const current_cost = fromJust (mcurrent_cost)
return isList (current_cost) ? subscript (current_cost) (index) : Nothing
}
case SpecialAbilityId.TraditionWitches: {
// There are two disadvantages that, when active, decrease the cost of
// this tradition by 10 AP each
const decreaseCost = (id: string) => (cost: number) =>
isDisadvantageActive (id) (hero) ? cost - 10 : cost
return pipe_ (
mcurrent_cost,
misNumberM,
fmap (pipe (
decreaseCost (DisadvantageId.NoFlyingBalm),
decreaseCost (DisadvantageId.NoFamiliar)
))
)
}
case SpecialAbilityId.Recherchegespuer: {
// The AP cost for this SA consist of two parts: AP based on the IC of
// the main subject (from "SA_531"/Wissensdurst) in addition to AP based
// on the IC of the side subject selected in this SA.
const mhero_entry_SA_531 = lookup<string> (SpecialAbilityId.Wissensdurst)
(HA.specialAbilities (hero))
if (isNothing (mhero_entry_SA_531)) {
return Nothing
}
if (isNothing (mcurrent_cost) || isNothing (mcurrent_cost)) {
return Nothing
}
const current_cost = fromJust (mcurrent_cost)
if (isNumber (current_cost)) {
return Nothing
}
const hero_entry_SA_531 = fromJust (mhero_entry_SA_531)
const getCostFromHeroEntry =
pipe (
ActiveObject.AL.sid,
misStringM,
bindF (lookupF (StaticData.A.skills (wiki))),
bindF (pipe (Skill.A.ic, icToIx, subscript (current_cost)))
)
return liftM2 (add)
(getCostFromHeroEntry (entry))
(pipe_ (
hero_entry_SA_531,
ActivatableDependent.A.active,
listToMaybe,
bindF (getCostFromHeroEntry)
))
}
case SpecialAbilityId.Handwerkskunst:
case SpecialAbilityId.KindDerNatur:
case SpecialAbilityId.KoerperlichesGeschick:
case SpecialAbilityId.SozialeKompetenz:
case SpecialAbilityId.Universalgenie: {
return pipe_ (
List (mcurrent_sid, mcurrent_sid2, mcurrent_sid3),
mapMaybe (sid => getCostForEntryWithSkillSel (misStringM)
(wiki)
(sid)
(mcurrent_cost)),
sum,
ensure (gt (0))
)
}
default: {
if (any (notNull) (Advantage.AL.select (wiki_entry)) && isNothing (mcurrent_cost)) {
return getSelectOptionCost (wiki_entry) (mcurrent_sid)
}
return mcurrent_cost
}
}
}
/**
* Returns the AP value and if the entry is an automatic entry
*/
const getTotalCost =
(isEntryToAdd: boolean) =>
(automatic_advantages: List<string>) =>
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(entry: Record<ActiveObjectWithId>) =>
(hero_entry: Maybe<Record<ActivatableDependent>>) =>
(wiki_entry: Activatable): Pair<number | List<number>, boolean> => {
const custom_cost = AOWIA.cost (entry)
const is_automatic = List.elem (AAL.id (wiki_entry)) (automatic_advantages)
if (isJust (custom_cost)) {
const is_disadvantage = Disadvantage.is (wiki_entry)
return Pair (is_disadvantage ? -fromJust (custom_cost) : fromJust (custom_cost), is_automatic)
}
const mentry_specifc_cost = getEntrySpecificCost (isEntryToAdd)
(wiki)
(hero)
(wiki_entry)
(hero_entry)
(entry)
const current_cost = fromMaybe<number | List<number>> (0) (mentry_specifc_cost)
if (isDisadvantage (wiki_entry)) {
return Pair (
isList (current_cost) ? map (negate) (current_cost) : -current_cost,
is_automatic
)
}
return Pair (current_cost, is_automatic)
}
/**
* Returns the AP you get when removing the ActiveObject.
*
* @param isEntryToAdd If `entry` has not been added to the list of active
* entries yet, this must be `true`, otherwise `false`.
*/
export const getCost =
(isEntryToAdd: boolean) =>
(automatic_advantages: List<string>) =>
(wiki: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(entry: Record<ActiveObjectWithId>): Maybe<Pair<number | List<number>, boolean>> => {
const current_id = AOWIA.id (entry)
return pipe_ (
getWikiEntry (wiki) (current_id),
bindF (ensure (isActivatableWikiEntry)),
fmap (getTotalCost (isEntryToAdd)
(automatic_advantages)
(wiki)
(hero)
(entry)
(bind (getHeroStateItem (hero) (current_id))
(ensure (isActivatableDependent))))
)
}
/**
* Uses the results from `getCost` saved in `ActivatableNameCost` to calculate
* the final cost value (and no list).
*/
const putCurrentCost =
(entry: Record<ActivatableNameCost>): Record<ActivatableNameCostSafeCost> =>
over (ActivatableNameCostL.finalCost)
((current_cost): number => {
const current_id = ActivatableNameCostA_.id (entry)
const category = getCategoryById (current_id)
const mcurrent_level = ActivatableNameCostA_.tier (entry)
// If the AP cost is still a List, and it is a Special Ability, it
// must be a list that represents the cost *for* each level separate,
// thus all relevant values must be summed up. In case of an
// advantage or disadvantage, it represents the cost *at* each level,
// so it does not need to be accumulated.
if (isList (current_cost)) {
const current_level = fromMaybe (1) (mcurrent_level)
if (elem (Category.SPECIAL_ABILITIES) (category)) {
return ifoldr (i => i <= (current_level - 1) ? add : cnst (ident as ident<number>))
(0)
(current_cost)
}
return fromMaybe (0) (subscript (current_cost) (current_level - 1))
}
// Usually, a single AP value represents the value has to be
// multiplied by the level to result in the final cost. There are two
// disadvantages where this is not valid.
if (
isJust (mcurrent_level)
&& current_id !== DisadvantageId.Principles
&& current_id !== DisadvantageId.Obligations
&& isNothing (ActivatableNameCostA_.customCost (entry))
) {
return maybe (0) (multiply (current_cost)) (mcurrent_level)
}
return current_cost
})
(entry) as Record<ActivatableNameCostSafeCost>
/**
* Gets the level string that has to be appended to the name.
*/
const getLevel = (level: number) => `${nbsp}${toRoman (level)}`
/**
* Gets the level string that has to be appended to the name. For special
* abilities, where levels are bought separately, this means it has to display a
* range when multiple levels have been bought.
*/
const getSpecialAbilityLevel =
(level: number) => level > 1 ? `${nbsp}I${nobr}${nobr}${toRoman (level)}` : getLevel (level)
/**
* Id-based check if the entry is a special ability.
*/
const isSpecialAbilityById =
pipe (getCategoryById, elem<Category> (Category.SPECIAL_ABILITIES))
/**
* Gets the level string that hast to be appended to the name. This string is
* aware of differences between dis/advantages and special abilties as well as
* it handles the Native Tongue level for languages.
*/
const getFinalLevelName =
(staticData: StaticDataRecord) =>
(entry: Record<ActivatableNameCost>) =>
/**
* @param current_level This is the same value from param `entry`, but this is
* ensured to be a number.
*/
(current_level: number) => {
const current_id = ActivatableNameCostA_.id (entry)
const current_cost = ActivatableNameCost.A.finalCost (entry)
if (current_id === SpecialAbilityId.Language && current_level === 4) {
return `${nbsp}${translate (staticData) ("specialabilities.nativetonguelevel")}`
}
if (isList (current_cost) || isSpecialAbilityById (current_id)) {
return getSpecialAbilityLevel (current_level)
}
return getLevel (current_level)
}
/**
* Returns a `Just` of the level string, if no level string available, returns
* `Nothing`.
*/
const getLevelNameIfValid =
(staticData: StaticDataRecord) =>
(entry: Record<ActivatableNameCost>): Maybe<string> => {
const current_id = ActivatableNameCostA_.id (entry)
const mcurrent_level = ActivatableNameCostA_.tier (entry)
if (isJust (mcurrent_level)
&& notElem (current_id) (List (DisadvantageId.Principles, DisadvantageId.Obligations))) {
return Just (getFinalLevelName (staticData) (entry) (fromJust (mcurrent_level)))
}
return Nothing
}
export const putLevelName =
(addLevelToName: boolean) =>
(staticData: StaticDataRecord) =>
(entry: Record<ActivatableNameCost>): Record<ActivatableNameCost> =>
pipe_ (
entry,
getLevelNameIfValid (staticData),
fmap ((levelName: string) => addLevelToName
? pipe_ (
entry,
over (ActivatableNameCostL_.name)
(flip (appendStr) (levelName)),
set (ActivatableNameCostL_.levelName)
(Just (levelName))
)
: set (ActivatableNameCostL_.levelName)
(Just (levelName))
(entry)),
fromMaybe (entry)
)
/**
* Calculates level name and level-based cost and (optionally) updates `name`.
* @param locale
* @param addLevelToName If true, does not add `current_level` to `name`.
*/
export const convertPerTierCostToFinalCost =
(addLevelToName: boolean) =>
(staticData: StaticDataRecord) =>
pipe (
putLevelName (addLevelToName) (staticData),
putCurrentCost
)
export const getActiveWithNoCustomCost =
filter (pipe (ActiveObject.A.cost, isNothing))
@@ -1,470 +0,0 @@
import { equals } from "../../../Data/Eq"
import { cnst, ident, join } from "../../../Data/Function"
import { fmap } from "../../../Data/Functor"
import { elem, filter, List, sum } from "../../../Data/List"
import { bindF, ensure, maybe, Maybe, thenF } from "../../../Data/Maybe"
import { add, multiply } from "../../../Data/Num"
import { foldr, lookupF } from "../../../Data/OrderedMap"
import { OrderedSet } from "../../../Data/OrderedSet"
import { Record } from "../../../Data/Record"
import { bimap, first, fst, Pair, second, snd } from "../../../Data/Tuple"
import { curryN3 } from "../../../Data/Tuple/Curry"
import { Category } from "../../Constants/Categories"
import { AdvantageIdsNoMaxInfl } from "../../Constants/Ids"
import { ActivatableSkillDependent } from "../../Models/ActiveEntries/ActivatableSkillDependent"
import { AttributeDependent } from "../../Models/ActiveEntries/AttributeDependent"
import { SkillDependent } from "../../Models/ActiveEntries/SkillDependent"
import { Energies } from "../../Models/Hero/Energies"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { PermanentEnergyLossAndBoughtBack } from "../../Models/Hero/PermanentEnergyLossAndBoughtBack"
import { ActiveActivatable, ActiveActivatableA_ } from "../../Models/View/ActiveActivatable"
import { AdventurePointsCategories } from "../../Models/View/AdventurePointsCategories"
import { Advantage } from "../../Models/Wiki/Advantage"
import { CombatTechnique } from "../../Models/Wiki/CombatTechnique"
import { Disadvantage } from "../../Models/Wiki/Disadvantage"
import { LiturgicalChant } from "../../Models/Wiki/LiturgicalChant"
import { Profession } from "../../Models/Wiki/Profession"
import { ProfessionVariant } from "../../Models/Wiki/ProfessionVariant"
import { Race } from "../../Models/Wiki/Race"
import { Skill } from "../../Models/Wiki/Skill"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { Spell } from "../../Models/Wiki/Spell"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { MatchingScriptAndLanguageRelated } from "../../Selectors/activatableSelectors"
import { getAllActiveByCategory } from "../Activatable/activatableActiveUtils"
import { getAPForActivatation, getAPForRange, ImprovementCost } from "../ImprovementCost"
import { pipe, pipe_ } from "../pipe"
import { getAdventurePointsSpentDifference } from "./adventurePointsUtils"
const SDA = StaticData.A
const HA = HeroModel.A
// Attributes
const getAPForAttribute = pipe (
AttributeDependent.A.value,
curryN3 (getAPForRange) (ImprovementCost.E) (8)
)
export const getAPSpentForAttributes = foldr (pipe (getAPForAttribute, add)) (0)
// Skills
const getAPForSkill =
(x: Record<Skill>) =>
pipe (SkillDependent.A.value, curryN3 (getAPForRange) (Skill.A.ic (x)) (0))
type skillsFold = (x: Record<SkillDependent>) => (s: number) => number
export const getAPSpentForSkills =
(xmap: StaticData["skills"]) =>
foldr (join (pipe (
SkillDependent.A.id,
lookupF (xmap),
maybe<skillsFold> (cnst (ident))
(x => pipe (getAPForSkill (x), add))
)))
(0)
// Combat Techniques
const getAPForCombatTechnique =
(x: Record<CombatTechnique>) =>
pipe (SkillDependent.A.value, curryN3 (getAPForRange) (CombatTechnique.A.ic (x)) (6))
export const getAPSpentForCombatTechniques =
(xmap: StaticData["combatTechniques"]) =>
foldr (join (pipe (
SkillDependent.A.id,
lookupF (xmap),
maybe<skillsFold> (cnst (ident))
(x => pipe (getAPForCombatTechnique (x), add))
)))
(0)
// Spells / Liturgical Chants
const getAPForSpellOrChant =
(x: Record<Spell> | Record<LiturgicalChant>) =>
(asd: Record<ActivatableSkillDependent>) =>
ActivatableSkillDependent.A.active (asd)
? pipe_ (
asd,
ActivatableSkillDependent.A.value,
curryN3 (getAPForRange) (Spell.AL.ic (x)) (0),
add (getAPForActivatation (Spell.AL.ic (x)))
)
: 0
type actSkillsFold = (x: Record<ActivatableSkillDependent>) => (s: number) => number
export const getAPSpentForSpells =
(xmap: StaticData["spells"]) =>
foldr (join (pipe (
ActivatableSkillDependent.A.id,
lookupF (xmap),
maybe<actSkillsFold> (cnst (ident))
(x => pipe (getAPForSpellOrChant (x), add))
)))
(0)
export const getAPSpentForLiturgicalChants =
(xmap: StaticData["liturgicalChants"]) =>
foldr (join (pipe (
ActivatableSkillDependent.A.id,
lookupF (xmap),
maybe<actSkillsFold> (cnst (ident))
(x => pipe (getAPForSpellOrChant (x), add))
)))
(0)
// Cantrips / Blessings
export const getAPSpentForCantrips = OrderedSet.size
export const getAPSpentForBlessings = OrderedSet.size
// Advantages / Disadvantages / Special Abilities
type ActiveAdvantage = Record<ActiveActivatable<Advantage>>
type ActiveDisadvantage = Record<ActiveActivatable<Disadvantage>>
type ActiveSpecialAbility = Record<ActiveActivatable<SpecialAbility>>
const AAA_ = ActiveActivatableA_
/**
* The return value is a triple:
*
* - The second value is the simple AP cost sum that needs to be subtracted from
* the amount of total AP. Automatic advantage costs are exluded here.
* - The first value is like the first, but it is the value that needs to be
* compared with the AP max for advantages. Some entries do not count towards
* this max. But automatic advantages are relevant here.
*/
export const getAPSpentForAdvantages =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["advantages"]) =>
(active: List<ActiveAdvantage>) =>
pipe_ (
active,
List.foldr ((e: Record<ActiveActivatable<Advantage>>): ident<Pair<number, number>> =>
AAA_.isAutomatic (e)
? first (add (AAA_.finalCost (e)))
: elem (AAA_.id (e)) (AdvantageIdsNoMaxInfl)
? second (add (AAA_.finalCost (e)))
: bimap (add (AAA_.finalCost (e)))
(add (AAA_.finalCost (e))))
(Pair (0, 0)),
join <ident<number>, ident<Pair<number, number>>>
(bimap)
(add (getAdventurePointsSpentDifference (wiki) (xmap) (active)))
)
export const getAPSpentForMagicalAdvantages =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["advantages"]) =>
pipe (
filter<ActiveAdvantage> (pipe (
ActiveActivatable.A.wikiEntry,
Advantage.AL.gr,
equals (2)
)) as ident<List<ActiveAdvantage>>,
getAPSpentForAdvantages (wiki) (xmap)
)
export const getAPSpentForBlessedAdvantages =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["advantages"]) =>
pipe (
filter<ActiveAdvantage> (pipe (
ActiveActivatable.A.wikiEntry,
Advantage.AL.gr,
equals (3)
)) as ident<List<ActiveAdvantage>>,
getAPSpentForAdvantages (wiki) (xmap)
)
/**
* Returns (AP spent on disadvantages, actual AP lost)
*/
export const getAPSpentForDisadvantages =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["disadvantages"]) =>
(active: List<ActiveDisadvantage>): Pair<number, number> =>
pipe_ (
active,
List.foldr ((e: Record<ActiveActivatable<Disadvantage>>): ident<Pair<number, number>> =>
AAA_.isAutomatic (e)
? first (add (AAA_.finalCost (e)))
: bimap (add (AAA_.finalCost (e)))
(add (AAA_.finalCost (e))))
(Pair (0, 0)),
join <ident<number>, ident<Pair<number, number>>>
(bimap)
(add (getAdventurePointsSpentDifference (wiki) (xmap) (active)))
)
export const getAPSpentForMagicalDisadvantages =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["disadvantages"]) =>
pipe (
filter<ActiveDisadvantage> (pipe (
ActiveActivatable.A.wikiEntry,
Disadvantage.AL.gr,
equals (2)
)) as ident<List<ActiveDisadvantage>>,
getAPSpentForDisadvantages (wiki) (xmap)
)
export const getAPSpentForBlessedDisadvantages =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["disadvantages"]) =>
pipe (
filter<ActiveDisadvantage> (pipe (
ActiveActivatable.A.wikiEntry,
Disadvantage.AL.gr,
equals (3)
)) as ident<List<ActiveDisadvantage>>,
getAPSpentForDisadvantages (wiki) (xmap)
)
export const getAPSpentForSpecialAbilities =
(wiki: StaticDataRecord) =>
(xmap: HeroModel["specialAbilities"]) =>
(active: List<ActiveSpecialAbility>) =>
pipe_ (
active,
List.foldr<Record<ActiveActivatable<SpecialAbility>>, number>
(pipe (ActiveActivatableA_.finalCost, add))
(0),
add (getAdventurePointsSpentDifference (wiki)
(xmap)
(active))
)
export const getAPSpentForEnergies =
(energies: Record<Energies>) => {
const addedArcaneEnergyCost =
pipe_ (
energies,
Energies.A.addedArcaneEnergyPoints,
curryN3 (getAPForRange) (ImprovementCost.D) (0)
)
const addedKarmaPointsCost =
pipe_ (
energies,
Energies.A.addedKarmaPoints,
curryN3 (getAPForRange) (ImprovementCost.D) (0)
)
const addedLifePointsCost =
pipe_ (
energies,
Energies.A.addedLifePoints,
curryN3 (getAPForRange) (ImprovementCost.D) (0)
)
const boughtBackArcaneEnergyCost =
pipe_ (
energies,
Energies.A.permanentArcaneEnergyPoints,
PermanentEnergyLossAndBoughtBack.A.redeemed,
multiply (2)
)
const boughtBackKarmaPointsCost =
pipe_ (
energies,
Energies.A.permanentKarmaPoints,
PermanentEnergyLossAndBoughtBack.A.redeemed,
multiply (2)
)
return sum (List (
addedArcaneEnergyCost,
addedKarmaPointsCost,
addedLifePointsCost,
boughtBackArcaneEnergyCost,
boughtBackKarmaPointsCost
))
}
export const getAPSpentForRace =
(wiki: StaticDataRecord) =>
pipe (
bindF (lookupF (SDA.races (wiki))),
maybe (0) (Race.A.ap)
)
/**
* Pass wiki, profession id, optional profession variant id and the current
* phase.
*/
export const getAPSpentForProfession =
(wiki: StaticDataRecord) =>
(mprofId: Maybe<string>) =>
(mprofVarId: Maybe<string>) =>
pipe (
ensure (equals (1)),
thenF (mprofId),
bindF (lookupF (SDA.professions (wiki))),
fmap (pipe (
Profession.A.ap,
Maybe.sum,
add (pipe_ (
mprofVarId,
bindF (lookupF (SDA.professionVariants (wiki))),
fmap (ProfessionVariant.A.ap),
Maybe.sum
))
))
)
export const getAPObject =
(staticData: StaticDataRecord) =>
(hero: HeroModelRecord) =>
(automatic_advantages: List<string>) =>
(matchingScriptAndLanguageRelated: MatchingScriptAndLanguageRelated) => {
const total = HA.adventurePointsTotal (hero)
const spentOnAttributes = getAPSpentForAttributes (HA.attributes (hero))
const spentOnSkills = getAPSpentForSkills (SDA.skills (staticData))
(HA.skills (hero))
const spentOnCombatTechniques =
getAPSpentForCombatTechniques (SDA.combatTechniques (staticData))
(HA.combatTechniques (hero))
const spentOnSpells = getAPSpentForSpells (SDA.spells (staticData))
(HA.spells (hero))
const spentOnLiturgicalChants =
getAPSpentForLiturgicalChants (SDA.liturgicalChants (staticData))
(HA.liturgicalChants (hero))
const spentOnCantrips = getAPSpentForCantrips (HA.cantrips (hero))
const spentOnBlessings = getAPSpentForBlessings (HA.blessings (hero))
const spentOnEnergies = getAPSpentForEnergies (HA.energies (hero))
const spentOnRace = getAPSpentForRace (staticData) (HA.race (hero))
const spentOnProfession = getAPSpentForProfession (staticData)
(HA.profession (hero))
(HA.professionVariant (hero))
(HA.phase (hero))
const spentOnSpecialAbilities =
getAPSpentForSpecialAbilities (staticData)
(HA.specialAbilities (hero))
(getAllActiveByCategory (Category.SPECIAL_ABILITIES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spentOnAdvantages =
getAPSpentForAdvantages (staticData)
(HA.advantages (hero))
(getAllActiveByCategory (Category.ADVANTAGES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spentOnMagicalAdvantages =
getAPSpentForMagicalAdvantages (staticData)
(HA.advantages (hero))
(getAllActiveByCategory (Category.ADVANTAGES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spentOnBlessedAdvantages =
getAPSpentForBlessedAdvantages (staticData)
(HA.advantages (hero))
(getAllActiveByCategory (Category.ADVANTAGES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spentOnDisadvantages =
getAPSpentForDisadvantages (staticData)
(HA.disadvantages (hero))
(getAllActiveByCategory (Category.DISADVANTAGES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spentOnMagicalDisadvantages =
getAPSpentForMagicalDisadvantages (staticData)
(HA.disadvantages (hero))
(getAllActiveByCategory (Category.DISADVANTAGES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spentOnBlessedDisadvantages =
getAPSpentForBlessedDisadvantages (staticData)
(HA.disadvantages (hero))
(getAllActiveByCategory (Category.DISADVANTAGES)
(false)
(automatic_advantages)
(matchingScriptAndLanguageRelated)
(staticData)
(hero))
const spent =
spentOnAttributes
+ spentOnSkills
+ spentOnCombatTechniques
+ spentOnSpells
+ spentOnLiturgicalChants
+ spentOnCantrips
+ spentOnBlessings
+ spentOnEnergies
+ spentOnRace
+ Maybe.sum (spentOnProfession)
+ spentOnSpecialAbilities
+ snd (spentOnAdvantages)
+ snd (spentOnMagicalAdvantages)
+ snd (spentOnBlessedAdvantages)
+ snd (spentOnDisadvantages)
+ snd (spentOnMagicalDisadvantages)
+ snd (spentOnBlessedDisadvantages)
return AdventurePointsCategories ({
total,
spent,
available: total - spent,
spentOnAttributes,
spentOnSkills,
spentOnCombatTechniques,
spentOnSpells,
spentOnLiturgicalChants,
spentOnCantrips,
spentOnBlessings,
spentOnEnergies,
spentOnRace,
spentOnProfession,
spentOnSpecialAbilities,
spentOnAdvantages: fst (spentOnAdvantages),
spentOnMagicalAdvantages: fst (spentOnMagicalAdvantages),
spentOnBlessedAdvantages: fst (spentOnBlessedAdvantages),
spentOnDisadvantages: fst (spentOnDisadvantages),
spentOnMagicalDisadvantages: fst (spentOnMagicalDisadvantages),
spentOnBlessedDisadvantages: fst (spentOnBlessedDisadvantages),
})
}
@@ -1,452 +0,0 @@
import { bool_ } from "../../../Data/Bool"
import { equals } from "../../../Data/Eq"
import { ident } from "../../../Data/Function"
import { fmap, fmapF } from "../../../Data/Functor"
import { any, countWith, countWithByKeyMaybe, elemF, find, flength, foldl, foldr, isList, List, notElem, subscript, take } from "../../../Data/List"
import { all, altF, bind, bindF, elem, ensure, fromJust, fromMaybe, guard, isJust, isNothing, Just, listToMaybe, Maybe, maybe, Nothing, or, sum, then } from "../../../Data/Maybe"
import { add, gt, inc, lt, multiply, negate, subtractBy } from "../../../Data/Num"
import { alter, empty, findWithDefault, lookup, OrderedMap } from "../../../Data/OrderedMap"
import { fromDefault, Record } from "../../../Data/Record"
import { fst, Pair, snd } from "../../../Data/Tuple"
import { AdvantageIdsNoMaxInfl, DisadvantageId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDependent } from "../../Models/ActiveEntries/ActivatableDependent"
import { ActiveObject } from "../../Models/ActiveEntries/ActiveObject"
import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel"
import { ActiveActivatable, ActiveActivatableAL_, ActiveActivatableA_ } from "../../Models/View/ActiveActivatable"
import { AdventurePointsCategories } from "../../Models/View/AdventurePointsCategories"
import { Disadvantage } from "../../Models/Wiki/Disadvantage"
import { Skill } from "../../Models/Wiki/Skill"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { Activatable } from "../../Models/Wiki/wikiTypeHelpers"
import { getSelectOptionCost } from "../Activatable/selectionUtils"
import { getMagicalTraditionsHeroEntries } from "../Activatable/traditionUtils"
import { getAPForActivatation } from "../ImprovementCost"
import { pipe, pipe_ } from "../pipe"
import { misNumberM, misStringM } from "../typeCheckUtils"
import { compareMaxLevel, compareSubMaxLevel, getActiveWithNoCustomCost } from "./activatableCostUtils"
const SDA = StaticData.A
const AP = AdventurePointsCategories.AL
const AAA = ActiveActivatable.A
const AOA = ActiveObject.A
/**
* Checks if there are enough AP available. If there are, returns `Nothing`.
* Otherwise returns a `Just` of the missing AP.
* @param negativeApValid If the character's AP left can be a negative value
* (during character creation) or not.
* @param availableAP The AP currently available.
* @param cost The AP value you want to check.
*/
export const getMissingAP =
(negativeApValid: boolean) => (cost: number) => (availableAP: number): Maybe<number> => {
if (cost > 0 && !negativeApValid) {
return cost <= availableAP ? Nothing : Just (cost - availableAP)
}
return Nothing
}
const semiTraditionIds =
List<string> (
SpecialAbilityId.TraditionArcaneBard,
SpecialAbilityId.TraditionArcaneDancer,
SpecialAbilityId.TraditionIntuitiveMage,
SpecialAbilityId.TraditionSavant,
SpecialAbilityId.TraditionZauberalchimisten,
SpecialAbilityId.TraditionAnimisten,
)
/**
* Returns the maximum AP value you can spend on magical/blessed
* advantages/disadvantages.
* @param state The list of dependent instances.
* @param index The index in the AP array. `0` equals General, `1` equals
* Magical and `2` equals Blessed.
*/
export const getDisAdvantagesSubtypeMax =
(isMagical: boolean) => (state: HeroModelRecord): number => {
if (isMagical) {
const mtradition =
listToMaybe (getMagicalTraditionsHeroEntries (HeroModel.AL.specialAbilities (state)))
const misSemiTradition =
fmap (pipe (ActivatableDependent.AL.id, elemF (semiTraditionIds)))
(mtradition)
if (or (misSemiTradition)) {
return 25
}
}
return 50
}
export interface MissingAPForDisAdvantage {
"@@name": "MissingAPForDisAdvantage"
totalMissing: Maybe<number>
mainMissing: Maybe<number>
subMissing: Maybe<number>
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const MissingAPForDisAdvantage =
fromDefault ("MissingAPForDisAdvantage")
<MissingAPForDisAdvantage> ({
totalMissing: Nothing,
mainMissing: Nothing,
subMissing: Nothing,
})
const getDisAdvantageSubtypeAPSpent =
(isBlessedOrMagical: Pair<boolean, boolean>) =>
(isDisadvantage: boolean) =>
(ap: Record<AdventurePointsCategories>): Maybe<number> => {
if (isDisadvantage) {
if (snd (isBlessedOrMagical)) {
return Just (AP.spentOnMagicalDisadvantages (ap))
}
if (fst (isBlessedOrMagical)) {
return Just (AP.spentOnBlessedDisadvantages (ap))
}
}
if (snd (isBlessedOrMagical)) {
return Just (AP.spentOnMagicalAdvantages (ap))
}
if (fst (isBlessedOrMagical)) {
return Just (AP.spentOnBlessedAdvantages (ap))
}
return Nothing
}
/**
* Checks if there are enough AP available and if the restrictions for
* advantages/disadvantages will be met.
* @param cost The AP value you want to check.
* @param ap The current AP state.
* @param state The list of dependent instances.
* @param isMagicalOrBlessed If the the advantage/disadvantage is magical or
* blessed.
* @param isDisadvantage If the entry is a disadvantage.
* @param isInCharacterCreation If the character's AP left can be a negative
* value (during character creation) or not.
*/
export const getMissingAPForDisAdvantage =
(isInCharacterCreation: boolean) =>
(isBlessedOrMagical: Pair<boolean, boolean>) =>
(isDisadvantage: boolean) =>
(hero: HeroModelRecord) =>
(ap: Record<AdventurePointsCategories>) =>
(id: string) =>
(cost: number): Record<MissingAPForDisAdvantage> => {
const currentAPSpent =
isDisadvantage
? AP.spentOnDisadvantages (ap)
: AP.spentOnAdvantages (ap)
const subCurrentAPSpent = getDisAdvantageSubtypeAPSpent (isBlessedOrMagical)
(isDisadvantage)
(ap)
const smallMax = getDisAdvantagesSubtypeMax (snd (isBlessedOrMagical)) (hero)
// a disadvantage has negative cost, but the sum to check is always positive
// (to be able to use one function for both advantages and disadvantages)
const normalizedCost = isDisadvantage ? cost * -1 : cost
// checks if there are enough AP below the max for the subtype
// (magical/blessed)
const subMissing =
isInCharacterCreation && notElem (id) (AdvantageIdsNoMaxInfl)
// (current + spent) - max > 0 => invalid
? bind (subCurrentAPSpent)
(pipe (
add (normalizedCost),
subtractBy (smallMax),
ensure (gt (0))
))
: Nothing
// Checks if there are enough AP below the max for advantages/disadvantages
const mainMissing =
isInCharacterCreation && notElem (id) (AdvantageIdsNoMaxInfl)
// (current + spent) - max > 0 => invalid
? ensure (gt (0)) (currentAPSpent + normalizedCost - 80)
: Nothing
// Checks if there are enough AP available in total
const totalMissing = getMissingAP (isInCharacterCreation)
(cost)
(AP.available (ap))
return MissingAPForDisAdvantage ({ totalMissing, mainMissing, subMissing })
}
const getPrinciplesObligationsDiff =
(id: string) =>
(staticData: StaticDataRecord) =>
(hero_slice: OrderedMap<string, Record<ActivatableDependent>>) =>
(entries: List<Record<ActiveActivatable>>): number => {
if (any (pipe (ActiveActivatableAL_.id, equals (id))) (entries)) {
return pipe_ (
hero_slice,
lookup (id),
bindF (entry => {
const current_active = ActivatableDependent.A.active (entry)
const current_max_level = foldl (compareMaxLevel)
(0)
(current_active)
const current_second_max_level = foldl (compareSubMaxLevel (current_max_level))
(0)
(current_active)
const at_max_level =
countWith (pipe (ActiveObject.A.tier, elem (current_max_level)))
(current_active)
const mbase_cost =
pipe_ (
staticData,
SDA.disadvantages,
lookup (id),
bindF (Disadvantage.A.cost),
misNumberM
)
return at_max_level > 1
? fmapF (mbase_cost) (base => current_max_level * -base)
: fmapF (mbase_cost) (base => current_second_max_level * -base)
}),
sum
)
}
return 0
}
const getPropertyOrAspectKnowledgeDiff =
(id: string) =>
pipe (
find (pipe (ActiveActivatableA_.id, equals (id))),
fmap ((entry: Record<ActiveActivatable>) => {
const current_active_length =
pipe_ (entry, ActiveActivatable.AL.heroEntry, ActivatableDependent.A.active, flength)
const mcost = pipe_ (entry, ActiveActivatable.AL.wikiEntry, Disadvantage.AL.cost)
if (isJust (mcost)) {
const cost = fromJust (mcost)
if (isList (cost)) {
const actualAPSum = pipe_ (cost, take (current_active_length), List.sum)
// Sum of *displayed* AP values for entries
const current_cost_sum =
sum (subscript (cost) (current_active_length - 1)) * current_active_length
return actualAPSum - current_cost_sum
}
}
return 0
}),
sum
)
/**
* `getSinglePersFlawDiff :: Int -> Int -> ActiveActivatable -> SID -> Int -> Int`
*
* `getSinglePersFlawDiff sid paid_entries entry current_sid current_entries`
*
* @param sid SID the diff is for.
* @param paid_entries Amount of active entries of the same SID that you get AP
* for.
* @param entry An entry of Personality Flaw.
* @param current_sid The current SID to check.
* @param current_entries The current amount of active entries of the current
* SID.
*/
const getSinglePersFlawDiff =
(sid: number) =>
(paid_entries: number) =>
(entry: Record<ActiveActivatable>) =>
(current_sid: number | string) =>
(current_entries: number): number =>
current_sid === sid && current_entries > paid_entries
? maybe (0)
(pipe (multiply (paid_entries), negate))
(getSelectOptionCost (AAA.wikiEntry (entry) as Activatable)
(Just (sid)))
: 0
const getPersonalityFlawsDiff =
(entries: List<Record<ActiveActivatable>>): number =>
pipe_ (
entries,
// Find any Personality Flaw entry, as all of them have the same list of
// active objects
find (pipe (ActiveActivatableA_.id, equals<string> (DisadvantageId.PersonalityFlaw))),
fmap ((entry: Record<ActiveActivatable>) => pipe_ (
entry,
ActiveActivatableA_.active,
countWithByKeyMaybe (e => then (guard (isNothing (AOA.cost (e))))
(AOA.sid (e))),
OrderedMap.foldrWithKey ((sid: string | number) => (val: number) =>
pipe_ (
List (
getSinglePersFlawDiff (7) (1) (entry) (sid) (val),
getSinglePersFlawDiff (8) (2) (entry) (sid) (val)
),
List.sum,
add
))
(0)
)),
// If no Personality Flaw was found, there's no diff.
Maybe.sum
)
const getBadHabitsDiff =
(staticData: StaticDataRecord) =>
(hero_slice: OrderedMap<string, Record<ActivatableDependent>>) =>
(entries: List<Record<ActiveActivatable>>): number =>
any (pipe (ActiveActivatableAL_.id, equals<string> (DisadvantageId.BadHabit))) (entries)
? sum (pipe_ (
hero_slice,
lookup<string> (DisadvantageId.PersonalityFlaw),
fmap (pipe (
// get current active
ActivatableDependent.A.active,
getActiveWithNoCustomCost,
flength,
gt (3),
bool_ (() => 0)
(() => pipe_ (
staticData,
SDA.disadvantages,
lookup<string> (DisadvantageId.BadHabit),
bindF (Disadvantage.A.cost),
misNumberM,
fmap (multiply (-3)),
sum
))
))
))
: 0
const getSkillSpecializationsDiff =
(staticData: StaticDataRecord) =>
(hero_slice: OrderedMap<string, Record<ActivatableDependent>>) =>
(entries: List<Record<ActiveActivatable>>): number => {
if (any (pipe (ActiveActivatableAL_.id, equals<string> (SpecialAbilityId.SkillSpecialization)))
(entries)) {
return sum (pipe_ (
hero_slice,
lookup<string> (SpecialAbilityId.SkillSpecialization),
fmap ((entry: Record<ActivatableDependent>) => {
const current_active = ActivatableDependent.A.active (entry)
// Count how many specializations are for the same skill
const sameSkill =
countWithByKeyMaybe (pipe (ActiveObject.A.sid, misStringM))
(current_active)
// Return the accumulated value, otherwise 0.
const getFlatSkillDone = findWithDefault (0)
// Calculates the diff for a single skill specialization
const getSingleDiff =
(accMap: OrderedMap<string, number>) =>
(sid: string) =>
(counter: number) =>
(skill: Record<Skill>) =>
getAPForActivatation (Skill.A.ic (skill))
* (getFlatSkillDone (sid) (accMap) + 1 - counter)
type TrackingPair = Pair<number, OrderedMap<string, number>>
// Iterates through the counter and sums up all cost differences for
// each specialization.
//
// It keeps track of how many specializations have been already
// taken into account.
const skillDone =
foldr (pipe (
ActiveObject.A.sid,
misStringM,
bindF (current_sid =>
fmapF (lookup (current_sid) (sameSkill))
(count => (p: TrackingPair) => {
const m = snd (p)
// Check if the value in the map is either
// Nothing or a Just of a lower number than
// the complete counter
// => which means there are still actions to
// be done
if (all (lt (count)) (lookup (current_sid) (m))) {
const mskill =
pipe_ (staticData, SDA.skills, lookup (current_sid))
return Pair (
fst (p) + sum (fmap (getSingleDiff (m)
(current_sid)
(count))
(mskill)),
alter (pipe (altF (Just (0)), fmap (inc)))
(current_sid)
(m)
)
}
return p
})),
fromMaybe<ident<TrackingPair>> (ident)
))
(Pair (0, empty))
(current_active)
return fst (skillDone)
})
))
}
return 0
}
const getPropertyKnowledgeDiff =
getPropertyOrAspectKnowledgeDiff (SpecialAbilityId.PropertyKnowledge)
const getAspectKnowledgeDiff =
getPropertyOrAspectKnowledgeDiff (SpecialAbilityId.AspectKnowledge)
/**
* The returned number modifies the current AP spent.
*/
export const getAdventurePointsSpentDifference =
(staticData: StaticDataRecord) =>
(hero_slice: OrderedMap<string, Record<ActivatableDependent>>) =>
(entries: List<Record<ActiveActivatable>>): number => {
const adventurePointsSpentDifferences = List (
getPrinciplesObligationsDiff (DisadvantageId.Principles) (staticData) (hero_slice) (entries),
getPrinciplesObligationsDiff (DisadvantageId.Obligations) (staticData) (hero_slice) (entries),
getPersonalityFlawsDiff (entries),
getBadHabitsDiff (staticData) (hero_slice) (entries),
getSkillSpecializationsDiff (staticData) (hero_slice) (entries),
getPropertyKnowledgeDiff (entries),
getAspectKnowledgeDiff (entries)
)
return List.sum (adventurePointsSpentDifferences)
}
@@ -1,102 +0,0 @@
import * as React from "react"
import { flip, thrush } from "../../../Data/Function"
import { fmapF } from "../../../Data/Functor"
import { fnull, List, map, toArray } from "../../../Data/List"
import { all, fromJust, Maybe, normalize, or } from "../../../Data/Maybe"
import { OrderedMap } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { ActivatableAddListItemContainer } from "../../Containers/InactiveActivatableContainer"
import { ActivatableActivationOptions } from "../../Models/Actions/ActivatableActivationOptions"
import { ActiveObjectWithId } from "../../Models/ActiveEntries/ActiveObjectWithId"
import { EntryRating } from "../../Models/Hero/heroTypeHelpers"
import { ActivatableNameCost } from "../../Models/View/ActivatableNameCost"
import { ActiveActivatable, ActiveActivatableA_ } from "../../Models/View/ActiveActivatable"
import { InactiveActivatable } from "../../Models/View/InactiveActivatable"
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { getFullName } from "../../Utilities/Activatable/activatableNameUtils"
import { pipe_ } from "../../Utilities/pipe"
import { isInactiveRated } from "../../Utilities/ratingUtils"
import { ListView } from "../Universal/List"
import { ListItem } from "../Universal/ListItem"
import { ListItemName } from "../Universal/ListItemName"
import { ListPlaceholder } from "../Universal/ListPlaceholder"
import { Scroll } from "../Universal/Scroll"
export interface ActivatableAddListProps {
hideGroup?: boolean
inactiveList: Maybe<List<Record<ActiveActivatable> | Record<InactiveActivatable>>>
staticData: StaticDataRecord
rating?: Maybe<OrderedMap<string, EntryRating>>
showRating?: boolean
selectedForInfo: Maybe<string>
addToList (args: Record<ActivatableActivationOptions>): void
selectForInfo (id: string): void
}
const AAA = ActiveActivatable.A
const AAA_ = ActiveActivatableA_
const IAA = InactiveActivatable.A
const ANCA = ActivatableNameCost.A
const AOWIA = ActiveObjectWithId.A
export function ActivatableAddList (props: ActivatableAddListProps) {
const {
hideGroup,
inactiveList: minactives,
staticData,
rating,
showRating: mshow_rating,
selectedForInfo,
addToList,
selectForInfo,
} = props
if (all (fnull) (minactives)) {
return <ListPlaceholder staticData={staticData} noResults type="inactiveSpecialAbilities" />
}
const normalizedRating = normalize (rating)
const isRatedWithRating = fmapF (Maybe (mshow_rating)) (flip (isInactiveRated) (normalizedRating))
return (
<Scroll>
<ListView>
{pipe_ (
minactives,
fromJust,
map (item => {
if (ActiveActivatable.is (item)) {
const name = getFullName (item)
const id = AAA_.id (item)
const index = pipe_ (item, AAA.nameAndCost, ANCA.active, AOWIA.index)
return (
<ListItem key={`${id}_${index}`} disabled>
<ListItemName name={name} />
</ListItem>
)
}
const isRatedForItem = fmapF (isRatedWithRating) (thrush (item))
return (
<ActivatableAddListItemContainer
key={IAA.id (item)}
hideGroup={hideGroup}
item={item}
isImportant={or (fmapF (isRatedForItem) (thrush (EntryRating.Essential)))}
isTypical={or (fmapF (isRatedForItem) (thrush (EntryRating.Common)))}
isUntypical={or (fmapF (isRatedForItem) (thrush (EntryRating.Uncommon)))}
selectedForInfo={selectedForInfo}
addToList={addToList}
selectForInfo={selectForInfo}
/>
)
}),
toArray
)}
</ListView>
</Scroll>
)
}
@@ -1,341 +0,0 @@
import * as React from "react"
import { List, notNullStr } from "../../../Data/List"
import { any, ensure, fromMaybe, guardReplace, isJust, isNothing, Just, Maybe, maybe, maybeToUndefined, Nothing, orN } from "../../../Data/Maybe"
import { lookup } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { fst, snd } from "../../../Data/Tuple"
import { AdvantageId, DisadvantageId, SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableActivationOptions } from "../../Models/Actions/ActivatableActivationOptions"
import { HeroModel } from "../../Models/Hero/HeroModel"
import { NumIdName } from "../../Models/NumIdName"
import { InactiveActivatable } from "../../Models/View/InactiveActivatable"
import { Advantage } from "../../Models/Wiki/Advantage"
import { Disadvantage } from "../../Models/Wiki/Disadvantage"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { getIdSpecificAffectedAndDispatchProps, getInactiveActivatableControlElements, InactiveActivatableControlElements, insertFinalCurrentCost, PropertiesAffectedByState } from "../../Utilities/Activatable/activatableInactiveViewUtils"
import { classListMaybe } from "../../Utilities/CSS"
import { translate, translateP } from "../../Utilities/I18n"
import { pipe_ } from "../../Utilities/pipe"
import { renderMaybeWith } from "../../Utilities/ReactUtils"
import { BasicInputDialog } from "../Universal/BasicInputDialog"
import { IconButton } from "../Universal/IconButton"
import { ListItem } from "../Universal/ListItem"
import { ListItemButtons } from "../Universal/ListItemButtons"
import { ListItemGroup } from "../Universal/ListItemGroup"
import { ListItemLeft } from "../Universal/ListItemLeft"
import { ListItemName } from "../Universal/ListItemName"
import { ListItemSeparator } from "../Universal/ListItemSeparator"
import { ListItemValues } from "../Universal/ListItemValues"
export interface ActivatableAddListItemOwnProps {
item: Record<InactiveActivatable>
isImportant?: boolean
isTypical?: boolean
isUntypical?: boolean
hideGroup?: boolean
selectedForInfo: Maybe<string>
addToList (args: Record<ActivatableActivationOptions>): void
selectForInfo (id: string): void
}
export interface ActivatableAddListItemStateProps {
skills: Maybe<HeroModel["skills"]>
staticData: StaticDataRecord
isEditingAllowed: boolean
}
export interface ActivatableAddListItemDispatchProps { }
export type ActivatableAddListItemProps =
ActivatableAddListItemStateProps
& ActivatableAddListItemDispatchProps
& ActivatableAddListItemOwnProps
export interface ActivatableAddListItemSelectedOptions {
selected: Maybe<string | number>
selected2: Maybe<string | number>
selected3: Maybe<string | number>
selectedTier: Maybe<number>
input: Maybe<string>
customCost: Maybe<string>
}
const IAA = InactiveActivatable.A
const IACEA = InactiveActivatableControlElements.A
const PABSA = PropertiesAffectedByState.A
export const ActivatableAddListItem: React.FC<ActivatableAddListItemProps> = props => {
const {
item,
isImportant,
isTypical,
isUntypical,
hideGroup,
selectForInfo,
selectedForInfo,
staticData,
addToList,
isEditingAllowed,
} = props
const id = IAA.id (item)
const [ mselected, setSelected ] = React.useState<Maybe<string | number>> (Nothing)
const [ mselected2, setSelected2 ] = React.useState<Maybe<string | number>> (Nothing)
const [ mselected3, setSelected3 ] = React.useState<Maybe<string | number>> (Nothing)
const [ mselected_level, setSelectedLevel ] = React.useState<Maybe<number>> (Nothing)
const [ minput, setInput ] = React.useState<Maybe<string>> (Nothing)
const [ mcustom_cost, setCustomCost ] = React.useState<Maybe<string>> (Nothing)
const [ mcustom_cost_preview, setCustomCostPreview ] = React.useState<Maybe<string>> (Nothing)
const [ showCustomCostDialog, setShowCustomCostDialog ] = React.useState<boolean> (false)
const isCustomCostAvailable = React.useMemo (
() => {
const staticEntry = IAA.wikiEntry (item)
return Advantage.is (staticEntry)
|| Disadvantage.is (staticEntry)
|| id === SpecialAbilityId.CustomSpecialAbility
},
[ item, id ]
)
const handleSelect =
React.useCallback (
(nextSelected: Maybe<string | number>) => {
setSelected (nextSelected)
setSelected2 (Nothing)
setSelected3 (Nothing)
},
[ setSelected, setSelected2, setSelected3 ]
)
const handleLevel =
React.useCallback (
(mlevel: Maybe<number>) => {
if (isJust (mlevel)) {
if (DisadvantageId.Principles === id || DisadvantageId.Obligations === id) {
setSelectedLevel (mlevel)
setSelected (Nothing)
}
else {
setSelectedLevel (mlevel)
}
}
},
[ setSelectedLevel, setSelected, id ]
)
const handleInput =
React.useCallback (
(input: string) => setInput (ensure (notNullStr) (input)),
[ setInput ]
)
const handleShowCustomCostDialog =
React.useCallback (
() => {
if (isCustomCostAvailable) {
setShowCustomCostDialog (true)
setCustomCostPreview (mcustom_cost)
}
},
[ setShowCustomCostDialog, setCustomCostPreview, isCustomCostAvailable, mcustom_cost ]
)
const handleCloseCustomCostDialog =
React.useCallback (
() => setShowCustomCostDialog (false),
[ setShowCustomCostDialog ]
)
const handleSetCustomCost =
React.useCallback (
() => setCustomCost (mcustom_cost_preview),
[ setCustomCost, mcustom_cost_preview ]
)
const handleSetCustomCostPreview =
React.useCallback (
(customCostStr: string) => pipe_ (customCostStr, ensure (notNullStr), setCustomCostPreview),
[ setCustomCostPreview ]
)
const handleDeleteCustomCost =
React.useCallback (
() => setCustomCost (Nothing),
[ setCustomCost ]
)
const selectElementDisabled =
([
AdvantageId.MagicalAttunement,
DisadvantageId.AfraidOf,
DisadvantageId.MagicalRestriction,
DisadvantageId.Principles,
DisadvantageId.BadHabit,
DisadvantageId.Stigma,
DisadvantageId.Obligations,
] as string[]) .includes (id)
&& any (notNullStr) (minput)
const selectedOptions: ActivatableAddListItemSelectedOptions = {
selected: mselected,
selected2: mselected2,
selected3: mselected3,
selectedTier: mselected_level,
input: minput,
customCost: mcustom_cost,
}
const propsAndActivationArgs =
getIdSpecificAffectedAndDispatchProps ({
handleInput,
handleSelect,
selectElementDisabled,
})
(staticData)
(item)
(selectedOptions)
const finalProps = insertFinalCurrentCost (item)
(selectedOptions)
(propsAndActivationArgs)
const controlElements =
getInactiveActivatableControlElements (staticData)
(isEditingAllowed)
({
handleInput,
handleSelect,
handleSecondSelect: setSelected2,
handleThirdSelect: setSelected3,
handleLevel,
selectElementDisabled,
})
(item)
(selectedOptions)
(finalProps)
const mlevelElementBefore = IACEA.levelElementBefore (controlElements)
const mlevelElementAfter = IACEA.levelElementAfter (controlElements)
const mselectElement = IACEA.selectElement (controlElements)
const msecondSelectElement = IACEA.secondSelectElement (controlElements)
const mthirdSelectElement = IACEA.thirdSelectElement (controlElements)
const minputElement = IACEA.inputElement (controlElements)
const mdisabled = IACEA.disabled (controlElements)
const handleAddToList =
React.useCallback (
() => {
const args = fst (finalProps)
addToList (args)
if (isJust (mselected) || isJust (mselected_level) || isJust (minput)) {
setInput (Nothing)
setSelected (Nothing)
setSelected2 (Nothing)
setSelected3 (Nothing)
setSelectedLevel (Nothing)
setCustomCost (Nothing)
}
},
[
mselected,
mselected_level,
minput,
setInput,
setSelected,
setSelected2,
setSelected3,
setSelectedLevel,
setCustomCost,
finalProps,
addToList,
]
)
const handleSelectForInfo =
React.useCallback (
() => selectForInfo (id),
[ selectForInfo, id ]
)
return (
<ListItem
important={isImportant}
recommended={isTypical}
unrecommended={isUntypical}
active={Maybe.elem (IAA.id (item)) (selectedForInfo)}
>
<ListItemLeft>
<ListItemName name={IAA.name (item)} onClick={handleSelectForInfo} />
{fromMaybe (null as React.ReactNode) (mlevelElementBefore)}
{fromMaybe (null as React.ReactNode) (mselectElement)}
{fromMaybe (null as React.ReactNode) (msecondSelectElement)}
{fromMaybe (null as React.ReactNode) (mthirdSelectElement)}
{fromMaybe (null as React.ReactNode) (minputElement)}
{fromMaybe (null as React.ReactNode) (mlevelElementAfter)}
</ListItemLeft>
<ListItemSeparator />
{orN (hideGroup)
? null
: (
<ListItemGroup
text={pipe_ (
staticData,
StaticData.A.specialAbilityGroups,
lookup (pipe_ (item, IAA.wikiEntry, SpecialAbility.AL.gr)),
maybe ("") (NumIdName.A.name)
)}
/>
)}
<ListItemValues>
<div
className={classListMaybe (List (
Just ("cost"),
guardReplace (isCustomCostAvailable) ("value-btn"),
guardReplace (isJust (mcustom_cost)) ("custom-cost")
))}
onClick={handleShowCustomCostDialog}
>
{renderMaybeWith ((x: number | string) => IAA.isAutomatic (item) ? `(${x})` : x)
(PABSA.currentCost (snd (finalProps)))}
</div>
<BasicInputDialog
id="custom-cost-dialog"
isOpen={showCustomCostDialog}
title={translate (staticData) ("advantagesdisadvantages.dialogs.customcost.title")}
description={
translateP (staticData)
("advantagesdisadvantages.dialogs.customcost.for")
(List (IAA.name (item)))
}
value={maybeToUndefined (mcustom_cost_preview)}
acceptLabel={translate (staticData) ("general.dialogs.donebtn")}
rejectLabel={translate (staticData) ("general.dialogs.deletebtn")}
rejectDisabled={isNothing (mcustom_cost)}
onClose={handleCloseCustomCostDialog}
onAccept={handleSetCustomCost}
onReject={handleDeleteCustomCost}
onChange={handleSetCustomCostPreview}
/>
</ListItemValues>
<ListItemButtons>
<IconButton
icon="&#xE916;"
disabled={mdisabled}
onClick={handleAddToList}
flat
/>
<IconButton
icon="&#xE912;"
onClick={handleSelectForInfo}
flat
/>
</ListItemButtons>
</ListItem>
)
}
@@ -1,95 +0,0 @@
import * as React from "react"
import { flip, thrush } from "../../../Data/Function"
import { fmapF } from "../../../Data/Functor"
import { fnull, List, map, toArray } from "../../../Data/List"
import { all, fromJust, Maybe, normalize, or } from "../../../Data/Maybe"
import { OrderedMap } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { ActivatableDeactivationOptions } from "../../Models/Actions/ActivatableDeactivationOptions"
import { EntryRating } from "../../Models/Hero/heroTypeHelpers"
import { ActiveActivatable, ActiveActivatableA_ } from "../../Models/View/ActiveActivatable"
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { pipe_ } from "../../Utilities/pipe"
import { isActiveRated } from "../../Utilities/ratingUtils"
import { ListView } from "../Universal/List"
import { ListPlaceholder } from "../Universal/ListPlaceholder"
import { Scroll } from "../Universal/Scroll"
import { ActivatableRemoveListItem } from "./ActivatableRemoveListItem"
export interface ActivatableRemoveListProps {
filterText: string
hideGroup?: boolean
list: Maybe<List<Record<ActiveActivatable>>>
staticData: StaticDataRecord
isRemovingEnabled: boolean
rating?: Maybe<OrderedMap<string, EntryRating>>
showRating?: boolean
selectedForInfo: Maybe<string>
setLevel (id: string, index: number, level: number): void
removeFromList (args: Record<ActivatableDeactivationOptions>): void
selectForInfo (id: string): void
}
const AAA_ = ActiveActivatableA_
export function ActivatableRemoveList (props: ActivatableRemoveListProps) {
const {
filterText,
list: mactives,
staticData,
rating,
showRating: mshow_rating,
isRemovingEnabled,
selectedForInfo,
setLevel,
removeFromList,
selectForInfo,
hideGroup,
} = props
if (all (fnull) (mactives)) {
return (
<ListPlaceholder
staticData={staticData}
noResults={filterText.length > 0}
type="specialAbilities"
/>
)
}
const normalizedRating = normalize (rating)
const isRatedWithRating = fmapF (Maybe (mshow_rating)) (flip (isActiveRated) (normalizedRating))
return (
<Scroll>
<ListView>
{pipe_ (
mactives,
fromJust,
map (item => {
const isRatedForItem = fmapF (isRatedWithRating) (thrush (item))
return (
<ActivatableRemoveListItem
key={`${AAA_.id (item)}_${AAA_.index (item)}`}
staticData={staticData}
item={item}
isImportant={or (fmapF (isRatedForItem) (thrush (EntryRating.Essential)))}
isTypical={or (fmapF (isRatedForItem) (thrush (EntryRating.Common)))}
isUntypical={or (fmapF (isRatedForItem) (thrush (EntryRating.Uncommon)))}
isRemovingEnabled={isRemovingEnabled}
selectedForInfo={selectedForInfo}
setLevel={setLevel}
removeFromList={removeFromList}
selectForInfo={selectForInfo}
hideGroup={hideGroup}
/>
)
}),
toArray
)}
</ListView>
</Scroll>
)
}
@@ -1,229 +0,0 @@
import * as React from "react"
import { isString } from "util"
import { equals, notEquals } from "../../../Data/Eq"
import { onF } from "../../../Data/Function"
import { fmap, mapReplace } from "../../../Data/Functor"
import { cons, flength, List } from "../../../Data/List"
import { any, bindF, ensure, fromJust, fromMaybe, guard, INTERNAL_shallowEquals, isJust, Just, liftM2, listToMaybe, Maybe, maybe, orN } from "../../../Data/Maybe"
import { max, min } from "../../../Data/Num"
import { lookup } from "../../../Data/OrderedMap"
import { Record } from "../../../Data/Record"
import { SpecialAbilityId } from "../../Constants/Ids"
import { ActivatableDeactivationOptions } from "../../Models/Actions/ActivatableDeactivationOptions"
import { NumIdName } from "../../Models/NumIdName"
import { ActiveActivatable, ActiveActivatableA_ } from "../../Models/View/ActiveActivatable"
import { DropdownOption } from "../../Models/View/DropdownOption"
import { SpecialAbility } from "../../Models/Wiki/SpecialAbility"
import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel"
import { classListMaybe } from "../../Utilities/CSS"
import { translate } from "../../Utilities/I18n"
import { getLevelElementsWithMin } from "../../Utilities/levelUtils"
import { pipe, pipe_ } from "../../Utilities/pipe"
import { renderMaybe } from "../../Utilities/ReactUtils"
import { Dropdown } from "../Universal/Dropdown"
import { IconButton } from "../Universal/IconButton"
import { ListItem } from "../Universal/ListItem"
import { ListItemButtons } from "../Universal/ListItemButtons"
import { ListItemGroup } from "../Universal/ListItemGroup"
import { ListItemName } from "../Universal/ListItemName"
import { ListItemSelections } from "../Universal/ListItemSelections"
import { ListItemSeparator } from "../Universal/ListItemSeparator"
import { ListItemValues } from "../Universal/ListItemValues"
export interface ActivatableRemoveListItemProps {
item: Record<ActiveActivatable>
staticData: StaticDataRecord
isRemovingEnabled: boolean
hideGroup?: boolean
isImportant?: boolean
isTypical?: boolean
isUntypical?: boolean
selectedForInfo: Maybe<string>
setLevel (id: string, index: number, level: number): void
removeFromList (args: Record<ActivatableDeactivationOptions>): void
selectForInfo (id: string): void
}
const AAA = ActiveActivatable.A
const AAA_ = ActiveActivatableA_
const DOA = DropdownOption.A
const ActivatableRemoveListItem: React.FC<ActivatableRemoveListItemProps> = props => {
const {
isRemovingEnabled,
hideGroup,
item,
isImportant,
isTypical,
isUntypical,
staticData,
selectForInfo,
selectedForInfo,
removeFromList,
setLevel,
} = props
const id = AAA_.id (item)
const handleSelectTier = React.useCallback (
(mlevel: Maybe<number>) => {
if (isJust (mlevel)) {
const level = fromJust (mlevel)
setLevel (id, AAA_.index (item), level)
}
},
[ item, setLevel, id ]
)
const handleRemove = React.useCallback (
() => removeFromList (pipe_ (
item,
AAA_.finalCost,
cost => ActivatableDeactivationOptions ({
id,
index: AAA_.index (item),
cost,
})
)),
[ item, removeFromList, id ]
)
const handleSelectForInfo =
React.useCallback (
() => selectForInfo (id),
[ selectForInfo, id ]
)
const mlevel_element =
liftM2<number, number, JSX.Element | string>
(levels => level => {
const curr_min =
isRemovingEnabled
? pipe_ (item, AAA_.minLevel, maybe (1) (max (1)))
: level
const curr_max = pipe_ (item, AAA_.maxLevel, maybe (levels) (min (levels)))
const levelOptions = getLevelElementsWithMin (curr_min) (curr_max)
const levelOptionsWithMotherTongue =
id === SpecialAbilityId.Language
&& (level === 4 || isRemovingEnabled)
? cons (levelOptions)
(DropdownOption ({
id: Just (4),
name: translate (staticData) ("specialabilities.nativetonguelevel"),
}))
: levelOptions
return flength (levelOptionsWithMotherTongue) > 1
? (
<Dropdown
className="tiers"
value={Just (level)}
onChange={handleSelectTier}
options={levelOptionsWithMotherTongue}
/>
)
: pipe_ (
levelOptions,
listToMaybe,
fmap ((o: Record<DropdownOption<number>>) => ` ${DOA.name (o)}`),
renderMaybe
)
})
(AAA_.levels (item))
(AAA_.level (item))
const baseName = AAA_.baseName (item)
return (
<ListItem
important={isImportant}
recommended={isTypical}
unrecommended={isUntypical}
active={Maybe.elem (id) (selectedForInfo)}
>
<ListItemName
name={
any ((x: string | JSX.Element) => !isString (x)) (mlevel_element)
? maybe (baseName)
((add_str: string) =>
`${baseName} (${add_str})`)
(AAA_.addName (item))
: AAA_.name (item)
}
onClick={handleSelectForInfo}
/>
<ListItemSelections>
{pipe_ (
mlevel_element,
bindF (ensure ((l): l is JSX.Element => !isString (l))),
fromMaybe (<></>)
)}
</ListItemSelections>
<ListItemSeparator />
{orN (hideGroup)
? null
: (
<ListItemGroup
text={pipe_ (
staticData,
StaticData.A.specialAbilityGroups,
lookup (pipe_ (item, AAA.wikiEntry, SpecialAbility.AL.gr)),
maybe ("") (NumIdName.A.name)
)}
/>
)}
<ListItemValues>
<div
className={classListMaybe (List (
Just ("cost"),
pipe_ (
item,
AAA_.customCost,
bindF (pipe (notEquals (0), guard)),
mapReplace ("custom-cost")
)
))}
>
{AAA_.isAutomatic (item) ? `(${AAA_.finalCost (item)})` : AAA_.finalCost (item)}
</div>
</ListItemValues>
<ListItemButtons>
{isRemovingEnabled
? (
<IconButton
icon="&#xE90b;"
onClick={handleRemove}
disabled={AAA_.disabled (item)}
flat
/>
)
: null}
<IconButton icon="&#xE912;" onClick={handleSelectForInfo} flat />
</ListItemButtons>
</ListItem>
)
}
const ActivatableRemoveListItemM =
React.memo (
ActivatableRemoveListItem,
(prevProps, nextProps) => {
const curr_item = prevProps.item
const next_item = nextProps.item
return onF (AAA_.level) (equals) (curr_item) (next_item)
&& onF (AAA_.finalCost) (equals) (curr_item) (next_item)
&& prevProps.isRemovingEnabled === nextProps.isRemovingEnabled
&& onF (AAA_.minLevel) (equals) (curr_item) (next_item)
&& onF (AAA_.maxLevel) (equals) (curr_item) (next_item)
&& onF (AAA_.name) (equals) (curr_item) (next_item)
&& onF (AAA_.disabled) (equals) (curr_item) (next_item)
&& INTERNAL_shallowEquals (prevProps.selectedForInfo) (nextProps.selectedForInfo)
}
)
export { ActivatableRemoveListItemM as ActivatableRemoveListItem }
-121
View File
@@ -1,121 +0,0 @@
/**
* @module Data.Ix
*
* The `Ix` class is used to map a contiguous subrange of values in type onto
* integers.
*
* @author Lukas Obermann
*/
import { cons, List } from "./List"
import { inc } from "./Num"
import { show } from "./Show"
import { fst, Pair, snd } from "./Tuple"
// MODULE HELPER FUNCTIONS
const buildRangeList =
(u: number) => (x: number) => (xs: List<number>): List<number> =>
x > u ? xs : cons (buildRangeList (u) (inc (x)) (xs)) (x)
/**
* `range :: Int a => (a, a) -> [a]`
*
* The list of values in the subrange defined by a bounding pair.
*
* The first argument `(l,u)` is a pair specifying the lower and upper bounds of
* a contiguous subrange of values.
*/
export const range =
(b: Pair<number, number>): List<number> =>
buildRangeList (snd (b)) (fst (b)) (List.empty)
/**
* `rangeN :: Int a => (a, a) -> [a]`
*
* The list of values in the subrange defined by a bounding pair.
*
* The first argument `(l,u)` is a pair specifying the lower and upper bounds of
* a contiguous subrange of values.
*
* Native version of `range`.
*/
export const rangeN =
(l: number, u: number): List<number> =>
buildRangeList (u) (l) (List.empty)
/**
* `inRange :: Int a => (a, a) -> a -> Bool`
*
* Returns `True` the given subscript lies in the range defined the bounding
* pair.
*
* The first argument `(l,u)` is a pair specifying the lower and upper bounds of
* a contiguous subrange of values.
*/
export const inRange =
(b: Pair<number, number>) => (x: number): boolean =>
x >= fst (b) && x <= snd (b)
/**
* `index :: Int a => (a, a) -> a -> Int`
*
* The position of a subscript in the subrange.
*
* The first argument `(l,u)` is a pair specifying the lower and upper bounds of
* a contiguous subrange of values.
*/
export const index =
(b: Pair<number, number>) => (x: number): number => {
if (inRange (b) (x)) {
return x - fst (b)
}
throw new RangeError (
`Ix.index: Index (${show (x)}) out of range (${show (b)}`
)
}
/**
* `inRangeN :: Int a => (a, a) -> a -> Bool`
*
* Returns `True` the given subscript lies in the range defined the bounding
* pair.
*
* The first argument `(l,u)` is a pair specifying the lower and upper bounds of
* a contiguous subrange of values.
*
* Native version of `inRange`.
*/
export const inRangeN =
(l: number, u: number) => (x: number): boolean =>
x >= l && x <= u
/**
* `rangeSize :: Int a => (a, a) -> Int`
*
* The size of the subrange defined by a bounding pair.
*
* The first argument `(l,u)` is a pair specifying the lower and upper bounds of
* a contiguous subrange of values.
*/
export const rangeSize =
(b: Pair<number, number>): number =>
fst (b) <= snd (b) ? snd (b) - fst (b) + 1 : 0
// NAMESPACED FUNCTIONS
export const Ix = {
range,
index,
inRange,
rangeSize,
}
-39
View File
@@ -1,39 +0,0 @@
import { index, inRange, inRangeN, range, rangeN, rangeSize } from "../Ix"
import { List } from "../List"
import { Pair } from "../Tuple"
test ("range", () => {
expect (range (Pair (1, 5))) .toEqual (List (1, 2, 3, 4, 5))
expect (range (Pair (1, 1))) .toEqual (List (1))
expect (range (Pair (1, -2))) .toEqual (List ())
})
test ("rangeN", () => {
expect (rangeN (1, 5)) .toEqual (List (1, 2, 3, 4, 5))
expect (rangeN (1, 1)) .toEqual (List (1))
expect (rangeN (1, -2)) .toEqual (List ())
})
test ("index", () => {
expect (index (Pair (1, 5)) (3)) .toEqual (2)
expect (index (Pair (1, 5)) (1)) .toEqual (0)
expect (() => index (Pair (1, 5)) (-1)) .toThrow ()
})
test ("inRange", () => {
expect (inRange (Pair (1, 5)) (3)) .toEqual (true)
expect (inRange (Pair (1, 5)) (1)) .toEqual (true)
expect (inRange (Pair (1, 5)) (-1)) .toEqual (false)
})
test ("inRangeN", () => {
expect (inRangeN (1, 5) (3)) .toEqual (true)
expect (inRangeN (1, 5) (1)) .toEqual (true)
expect (inRangeN (1, 5) (-1)) .toEqual (false)
})
test ("rangeSize", () => {
expect (rangeSize (Pair (1, 5))) .toEqual (5)
expect (rangeSize (Pair (1, 1))) .toEqual (1)
expect (rangeSize (Pair (1, -2))) .toEqual (0)
})
+223
View File
@@ -0,0 +1,223 @@
import { ActionCreatorWithPayload } from "@reduxjs/toolkit"
import { Advantage } from "optolith-database-schema/types/Advantage"
import { Disadvantage } from "optolith-database-schema/types/Disadvantage"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { useCallback } from "react"
import {
DisplayedActiveActivatable,
getCostDifferenceOnRemove,
getCostOfInstance,
} from "../../shared/domain/activatable/activatableActive.ts"
import { ActivatableInstance } from "../../shared/domain/activatable/activatableEntry.ts"
import { equalsIdentifier } from "../../shared/domain/identifier.ts"
import { useTranslate } from "../../shared/hooks/translate.ts"
import { removeIndex } from "../../shared/utils/array.ts"
import { isOk } from "../../shared/utils/result.ts"
import { selectGetSelectOptionsById } from "../selectors/activatableSelectors.ts"
import { showAlert } from "../slices/alertsSlice.ts"
import { selectMagicalAndBlessedAdvantagesAndDisadvantagesCache } from "../slices/databaseSlice.ts"
import {
AvailabilityCheckResult,
RejectionReason,
getAlertForReason,
getSpecialAdventurePointsKind,
useAreEnoughAdventurePointsAvailableToBuy,
useAreEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage,
} from "./areEnoughAdventurePointsAvailableToBuy.ts"
import { useAppDispatch, useAppSelector } from "./redux.ts"
/**
* Returns a set of callbacks for interaction with inactive activatable entries.
*/
export const useInactiveActivatableActions = (
createAddAction: ActionCreatorWithPayload<{ id: number; instance: ActivatableInstance }>,
areEnoughAdventurePointsAvailableToBuy: (
id: number,
cost: number,
) => AvailabilityCheckResult<RejectionReason>,
) => {
const dispatch = useAppDispatch()
const translate = useTranslate()
const handleAdd = useCallback(
(params: { id: number; instance: ActivatableInstance }, cost: number) => {
const enoughAP = areEnoughAdventurePointsAvailableToBuy(params.id, cost)
if (isOk(enoughAP)) {
dispatch(createAddAction(params))
} else {
dispatch(showAlert(getAlertForReason(enoughAP.error, translate)))
}
},
[areEnoughAdventurePointsAvailableToBuy, createAddAction, dispatch, translate],
)
return {
handleAdd,
}
}
/**
* Returns a set of callbacks for interaction with inactive special ability
* entries.
*/
export const useInactiveSpecialAbilityActions = (
createAddAction: ActionCreatorWithPayload<{ id: number; instance: ActivatableInstance }>,
) => {
const areEnoughAdventurePointsAvailableToBuy = useAreEnoughAdventurePointsAvailableToBuy()
return useInactiveActivatableActions(createAddAction, (_id, cost) =>
areEnoughAdventurePointsAvailableToBuy(cost),
)
}
/**
* Returns a set of callbacks for interaction with inactive advantage and
* disadvantage entries.
*/
export const useInactiveAdvantageAndDisadvantageActions = (
kind: "advantage" | "disadvantage",
createAddAction: ActionCreatorWithPayload<{ id: number; instance: ActivatableInstance }>,
) => {
const areEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage =
useAreEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage()
const magicalAndBlessedAdvantagesAndDisadvantagesCache = useAppSelector(
selectMagicalAndBlessedAdvantagesAndDisadvantagesCache,
)
return useInactiveActivatableActions(createAddAction, (id, cost) =>
areEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage(
cost,
getSpecialAdventurePointsKind(magicalAndBlessedAdvantagesAndDisadvantagesCache, kind, id),
),
)
}
const useActiveActivatableActions = (
createRemoveAction: ActionCreatorWithPayload<{ id: number; index: number }>,
createChangeLevelAction: ActionCreatorWithPayload<{ id: number; index: number; level: number }>,
createIdObject: (id: number) => ActivatableIdentifier,
areEnoughAdventurePointsAvailableToBuy: (
id: number,
cost: number,
) => AvailabilityCheckResult<RejectionReason>,
) => {
const dispatch = useAppDispatch()
const translate = useTranslate()
const handleRemove = useCallback(
(
params: { id: number; index: number },
activeEntry: DisplayedActiveActivatable<Advantage | Disadvantage>,
) => {
const previousLevelCostDifference = getCostDifferenceOnRemove(activeEntry.cost)
const enoughAP = areEnoughAdventurePointsAvailableToBuy(
params.id,
previousLevelCostDifference,
)
if (isOk(enoughAP)) {
dispatch(createRemoveAction(params))
return true
} else {
dispatch(showAlert(getAlertForReason(enoughAP.error, translate)))
return false
}
},
[areEnoughAdventurePointsAvailableToBuy, createRemoveAction, dispatch, translate],
)
const getSelectOptionsById = useAppSelector(selectGetSelectOptionsById)
const handleChangeLevel = useCallback(
(
params: { id: number; index: number; level: number },
activeEntry: DisplayedActiveActivatable<Advantage | Disadvantage>,
): boolean => {
const idObject = createIdObject(params.id)
const previousLevelCostDifference = getCostDifferenceOnRemove(activeEntry.cost)
const nextLevelCost = getCostOfInstance(
idObject,
activeEntry.static.ap_value,
removeIndex(activeEntry.dynamic.instances, activeEntry.instanceIndex),
activeEntry.dynamic.instances[activeEntry.instanceIndex]!,
optionId => getSelectOptionsById(idObject)?.find(opt => equalsIdentifier(opt.id, optionId)),
true,
)
const enoughAP = areEnoughAdventurePointsAvailableToBuy(
params.id,
(nextLevelCost ?? 0) - previousLevelCostDifference,
)
if (isOk(enoughAP)) {
dispatch(createChangeLevelAction(params))
return true
} else {
dispatch(showAlert(getAlertForReason(enoughAP.error, translate)))
return false
}
},
[
areEnoughAdventurePointsAvailableToBuy,
createChangeLevelAction,
createIdObject,
dispatch,
getSelectOptionsById,
translate,
],
)
return {
handleRemove,
handleChangeLevel,
}
}
/**
* Returns a set of callbacks for interaction with active activatable entries.
*/
export const useActiveSpecialAbilityActions = (
createRemoveAction: ActionCreatorWithPayload<{ id: number; index: number }>,
createChangeLevelAction: ActionCreatorWithPayload<{ id: number; index: number; level: number }>,
createIdObject: (id: number) => ActivatableIdentifier,
) => {
const areEnoughAdventurePointsAvailableToBuy = useAreEnoughAdventurePointsAvailableToBuy()
return useActiveActivatableActions(
createRemoveAction,
createChangeLevelAction,
createIdObject,
(_id, cost) => areEnoughAdventurePointsAvailableToBuy(cost),
)
}
/**
* Returns a set of callbacks for interaction with inactive advantage and
* disadvantage entries.
*/
export const useActiveAdvantageAndDisadvantageActions = (
kind: "advantage" | "disadvantage",
createRemoveAction: ActionCreatorWithPayload<{ id: number; index: number }>,
createChangeLevelAction: ActionCreatorWithPayload<{ id: number; index: number; level: number }>,
createIdObject: (id: number) => ActivatableIdentifier,
) => {
const areEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage =
useAreEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage()
const magicalAndBlessedAdvantagesAndDisadvantagesCache = useAppSelector(
selectMagicalAndBlessedAdvantagesAndDisadvantagesCache,
)
return useActiveActivatableActions(
createRemoveAction,
createChangeLevelAction,
createIdObject,
(id, cost) =>
areEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage(
cost,
getSpecialAdventurePointsKind(magicalAndBlessedAdvantagesAndDisadvantagesCache, kind, id),
),
)
}
@@ -0,0 +1,374 @@
import { CacheMap } from "optolith-database-schema/config/cache"
import { useCallback } from "react"
import {
maximumAdventurePointsForAdventagesAndDisadvantages,
maximumAdventurePointsForBlessedAdventagesAndDisadvantages,
} from "../../shared/domain/activatable/advantagesDisadvantages.ts"
import {
AdventurePointsCache,
getTotalAdventurePointsFromCache,
} from "../../shared/domain/adventurePoints/cache.ts"
import { Result, combine, error, ok } from "../../shared/utils/result.ts"
import { Translate } from "../../shared/utils/translate.ts"
import { assertExhaustive } from "../../shared/utils/typeSafety.ts"
import {
selectAdventurePointsAvailable,
selectAdventurePointsSpentOnAdvantages,
selectAdventurePointsSpentOnBlessedAdvantages,
selectAdventurePointsSpentOnBlessedDisadvantages,
selectAdventurePointsSpentOnDisadvantages,
selectAdventurePointsSpentOnMagicalAdvantages,
selectAdventurePointsSpentOnMagicalDisadvantages,
} from "../selectors/adventurePointSelectors.ts"
import { selectMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages } from "../selectors/traditionSelectors.ts"
import { Alert } from "../slices/alertsSlice.ts"
import { useAppSelector } from "./redux.ts"
type SpecialAdventurePointsKind =
| "advantage"
| "magicalAdvantage"
| "blessedAdvantage"
| "disadvantage"
| "magicalDisadvantage"
| "blessedDisadvantage"
/**
* Result of an availability check. If the entry can be bought or increased,
* the result is `Ok`. Otherwise, the result is `Err` and states the reason why
* the entry cannot be bought or increased.
*/
export type AvailabilityCheckResult<T extends RejectionReason> = Result<number, T>
/**
* Reason why the requested entry cannot be bought or increased. It states how
* many adventure points are missing in which category.
*/
export type RejectionReason = GeneralRejectionReason | SpecialRejectionReason
/**
* Reason: Not enough adventure points are available in general.
*/
export type GeneralRejectionReason = {
kind: "general"
missingValue: number
}
/**
* Reason: Not enough adventure points are available for a specific advantage
* or disadvantage category limit.
*/
export type SpecialRejectionReason = {
kind: SpecialAdventurePointsKind
limit: number
missingValue: number
}
const checkGeneral = (
value: number,
available: number,
): AvailabilityCheckResult<GeneralRejectionReason> =>
available >= value ? ok(value) : error({ kind: "general", missingValue: value - available })
const checkAvailable = (
value: number,
available: number,
limit: number,
kind: SpecialAdventurePointsKind,
): AvailabilityCheckResult<SpecialRejectionReason> =>
available >= value ? ok(value) : error({ kind, missingValue: value - available, limit })
const checkLimitAndSpent = (
value: number,
limit: number,
spent: number,
kind: SpecialAdventurePointsKind,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkAvailable(value, limit - spent, limit, kind)
const checkAdvantages = (
value: number,
spentOnAdvantages: AdventurePointsCache,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkLimitAndSpent(
value,
maximumAdventurePointsForAdventagesAndDisadvantages,
getTotalAdventurePointsFromCache(spentOnAdvantages),
"advantage",
)
const checkMagicalAdvantages = (
value: number,
limit: number,
spentOnMagicalAdvantages: AdventurePointsCache,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkLimitAndSpent(
value,
limit,
getTotalAdventurePointsFromCache(spentOnMagicalAdvantages),
"magicalAdvantage",
)
const checkBlessedAdvantages = (
value: number,
spentOnBlessedAdvantages: AdventurePointsCache,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkLimitAndSpent(
value,
maximumAdventurePointsForBlessedAdventagesAndDisadvantages,
getTotalAdventurePointsFromCache(spentOnBlessedAdvantages),
"blessedAdvantage",
)
const checkDisadvantages = (
value: number,
spentOnDisadvantages: AdventurePointsCache,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkLimitAndSpent(
value,
maximumAdventurePointsForAdventagesAndDisadvantages,
-getTotalAdventurePointsFromCache(spentOnDisadvantages),
"disadvantage",
)
const checkMagicalDisadvantages = (
value: number,
limit: number,
spentOnMagicalDisadvantages: AdventurePointsCache,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkLimitAndSpent(
value,
limit,
-getTotalAdventurePointsFromCache(spentOnMagicalDisadvantages),
"magicalDisadvantage",
)
const checkBlessedDisadvantages = (
value: number,
spentOnBlessedDisadvantages: AdventurePointsCache,
): AvailabilityCheckResult<SpecialRejectionReason> =>
checkLimitAndSpent(
value,
maximumAdventurePointsForBlessedAdventagesAndDisadvantages,
-getTotalAdventurePointsFromCache(spentOnBlessedDisadvantages),
"blessedDisadvantage",
)
/**
* Returns an alert object for the given rejection reason.
*/
export const getAlertForReason = (reason: RejectionReason, translate: Translate): Alert => {
switch (reason.kind) {
case "general":
return {
title: translate("Not enough AP"),
description: translate(
"You are missing {0} Adventure Points to do this.",
reason.missingValue,
reason.missingValue,
),
}
case "advantage":
return {
title: translate("Exceeding Adventure Points limit for advantages"),
description: translate(
"You cannot spend more than {0} AP on advantages. You would exceed this limit by {1} AP.",
reason.limit,
reason.missingValue,
),
}
case "magicalAdvantage":
return {
title: translate("Exceeding Adventure Points limit for magical advantages"),
description: translate(
"You cannot spend more than {0} AP on magical advantages. You would exceed this limit by {1} AP.",
reason.limit,
reason.missingValue,
),
}
case "blessedAdvantage":
return {
title: translate("Exceeding Adventure Points limit for blessed advantages"),
description: translate(
"You cannot spend more than {0} AP on blessed advantages. You would exceed this limit by {1} AP.",
reason.limit,
reason.missingValue,
),
}
case "disadvantage":
return {
title: translate("Exceeding Adventure Points limit for disadvantages"),
description: translate(
"You cannot receive more than {0} AP from disadvantages. You would exceed this limit by {1} AP.",
reason.limit,
reason.missingValue,
),
}
case "magicalDisadvantage":
return {
title: translate("Exceeding Adventure Points limit for magical disadvantages"),
description: translate(
"You cannot receive more than {0} AP from magical disadvantages. You would exceed this limit by {1} AP.",
reason.limit,
reason.missingValue,
),
}
case "blessedDisadvantage":
return {
title: translate("Exceeding Adventure Points limit for blessed disadvantages"),
description: translate(
"You cannot receive more than {0} AP from blessed disadvantages. You would exceed this limit by {1} AP.",
reason.limit,
reason.missingValue,
),
}
default:
return assertExhaustive(reason)
}
}
const combineChecks = <T extends RejectionReason>(
firstResult: AvailabilityCheckResult<T>,
...nextResults: AvailabilityCheckResult<T>[]
): AvailabilityCheckResult<T> =>
nextResults.reduce(
(acc, res) =>
combine(
acc,
res,
ok1 => ok1,
err1 => err1,
),
firstResult,
)
/**
* Custom hook that provides a function that checks whether enough adventure
* points are available to buy or increase an entry.
*/
export const useAreEnoughAdventurePointsAvailableToBuy = () => {
const available = useAppSelector(selectAdventurePointsAvailable)
return useCallback(
(value: number): AvailabilityCheckResult<GeneralRejectionReason> =>
checkGeneral(value, available),
[available],
)
}
/**
* Returns the special adventure points kind for the given advantage or
* disadvantage identifier.
*/
export const getSpecialAdventurePointsKind = (
cache: CacheMap["magicalAndBlessedAdvantagesAndDisadvantages"],
kind: "advantage" | "disadvantage",
id: number,
): SpecialAdventurePointsKind => {
switch (kind) {
case "advantage":
if (cache.advantages.magical.ids.includes(id)) {
return "magicalAdvantage"
} else if (cache.advantages.blessed.ids.includes(id)) {
return "blessedAdvantage"
} else {
return "advantage"
}
case "disadvantage":
if (cache.disadvantages.magical.ids.includes(id)) {
return "magicalDisadvantage"
} else if (cache.disadvantages.blessed.ids.includes(id)) {
return "blessedDisadvantage"
} else {
return "disadvantage"
}
default:
return assertExhaustive(kind)
}
}
/**
* Custom hook that provides a function that checks whether enough adventure
* points are available to buy or sell an advantage or disadvantage entry.
*/
export const useAreEnoughAdventurePointsAvailableToBuyAnAdvantageOrDisadvantage = () => {
const maximumForMagicalAdvantagesDisadvantages = useAppSelector(
selectMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages,
)
const available = useAppSelector(selectAdventurePointsAvailable)
const spentOnAdvantages = useAppSelector(selectAdventurePointsSpentOnAdvantages)
const spentOnMagicalAdvantages = useAppSelector(selectAdventurePointsSpentOnMagicalAdvantages)
const spentOnBlessedAdvantages = useAppSelector(selectAdventurePointsSpentOnBlessedAdvantages)
const spentOnDisadvantages = useAppSelector(selectAdventurePointsSpentOnDisadvantages)
const spentOnMagicalDisadvantages = useAppSelector(
selectAdventurePointsSpentOnMagicalDisadvantages,
)
const spentOnBlessedDisadvantages = useAppSelector(
selectAdventurePointsSpentOnBlessedDisadvantages,
)
return useCallback(
(
value: number,
kind?: SpecialAdventurePointsKind,
): AvailabilityCheckResult<RejectionReason> => {
switch (kind) {
case undefined:
return checkGeneral(value, available)
case "advantage":
return combineChecks<RejectionReason>(
checkGeneral(value, available),
checkAdvantages(value, spentOnAdvantages),
)
case "magicalAdvantage":
return combineChecks<RejectionReason>(
checkGeneral(value, available),
checkAdvantages(value, spentOnAdvantages),
checkMagicalAdvantages(
value,
maximumForMagicalAdvantagesDisadvantages,
spentOnMagicalAdvantages,
),
)
case "blessedAdvantage":
return combineChecks<RejectionReason>(
checkGeneral(value, available),
checkAdvantages(value, spentOnAdvantages),
checkBlessedAdvantages(value, spentOnBlessedAdvantages),
)
case "disadvantage":
return combineChecks<RejectionReason>(
checkGeneral(value, available),
checkDisadvantages(-value, spentOnDisadvantages),
)
case "magicalDisadvantage":
return combineChecks<RejectionReason>(
checkGeneral(value, available),
checkDisadvantages(-value, spentOnDisadvantages),
checkMagicalDisadvantages(
-value,
maximumForMagicalAdvantagesDisadvantages,
spentOnMagicalDisadvantages,
),
)
case "blessedDisadvantage":
return combineChecks<RejectionReason>(
checkGeneral(value, available),
checkDisadvantages(-value, spentOnDisadvantages),
checkBlessedDisadvantages(-value, spentOnBlessedDisadvantages),
)
default:
return assertExhaustive(kind)
}
},
[
available,
maximumForMagicalAdvantagesDisadvantages,
spentOnAdvantages,
spentOnBlessedAdvantages,
spentOnBlessedDisadvantages,
spentOnDisadvantages,
spentOnMagicalAdvantages,
spentOnMagicalDisadvantages,
],
)
}
@@ -103,8 +103,12 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
return <InlineLibraryPlaceholder />
case "FatePointSexSpecialAbility":
return <InlineLibraryPlaceholder />
case "FatePointSpecialAbility":
return <InlineLibraryPlaceholder />
case "FocusRule":
return <InlineLibraryFocusRule id={id.focus_rule} />
case "FoolsHatEnchantment":
return <InlineLibraryPlaceholder />
case "GemOrPreciousStone":
return <InlineLibraryPlaceholder />
case "GeneralSpecialAbility":
@@ -145,6 +149,10 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
return <InlineLibraryPlaceholder />
case "MagicalMelody":
return <InlineLibraryPlaceholder />
case "MagicalRune":
return <InlineLibraryPlaceholder />
case "MagicalSign":
return <InlineLibraryPlaceholder />
case "MagicalSpecialAbility":
return <InlineLibraryPlaceholder />
case "MagicalTradition":
@@ -187,8 +195,14 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
return <InlineLibraryPlaceholder />
case "Skill":
return <InlineLibrarySkill id={id.skill} />
case "SkillStyleSpecialAbility":
return <InlineLibraryPlaceholder />
case "Spell":
return <InlineLibraryPlaceholder />
case "SpellSwordEnchantment":
return <InlineLibraryPlaceholder />
case "StaffEnchantment":
return <InlineLibraryPlaceholder />
case "ThievesTool":
return <InlineLibraryPlaceholder />
case "ToolOfTheTrade":
@@ -199,6 +213,8 @@ export const InlineLibraryRouter: FC<Props> = ({ id }) => {
return <InlineLibraryPlaceholder />
case "Trinkhornzauber":
return <InlineLibraryPlaceholder />
case "VampiricGift":
return <InlineLibraryPlaceholder />
case "Vehicle":
return <InlineLibraryPlaceholder />
case "Vision":
@@ -1,4 +1,8 @@
import { FC } from "react"
import {
maximumAdventurePointsForAdventagesAndDisadvantages,
maximumAdventurePointsForBlessedAdventagesAndDisadvantages,
} from "../../shared/domain/activatable/advantagesDisadvantages.ts"
import { useTranslate } from "../../shared/hooks/translate.ts"
import { useAppSelector } from "../hooks/redux.ts"
import {
@@ -78,7 +82,7 @@ export const AdventurePointsTooltip: FC = () => {
{translate(
"{0}/{1} AP spent on advantages",
spentOnAdvantages.general + spentOnAdvantages.bound,
80,
maximumAdventurePointsForAdventagesAndDisadvantages,
)}
</span>
<span>
@@ -95,32 +99,32 @@ export const AdventurePointsTooltip: FC = () => {
? translate(
"Thereof {0}/{1} on blessed advantages",
spentOnBlessedAdvantages.general + spentOnBlessedAdvantages.bound,
50,
maximumAdventurePointsForBlessedAdventagesAndDisadvantages,
)
: null}
</span>
<span>
{translate(
"{0}/{1} AP received from disadvantages",
spentOnDisadvantages.general + spentOnDisadvantages.bound,
80,
Math.abs(spentOnDisadvantages.general + spentOnDisadvantages.bound),
maximumAdventurePointsForAdventagesAndDisadvantages,
)}
</span>
<span>
{spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound > 0
{spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound < 0
? translate(
"Thereof {0}/{1} from magic disadvantages",
spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound,
Math.abs(spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound),
maximumForMagicalAdvantagesDisadvantages,
)
: null}
</span>
<span>
{spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound > 0
{spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound < 0
? translate(
"Thereof {0}/{1} from blessed disadvantages",
spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound,
50,
Math.abs(spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound),
maximumAdventurePointsForBlessedAdventagesAndDisadvantages,
)
: null}
</span>
+27
View File
@@ -0,0 +1,27 @@
import { FC, useCallback } from "react"
import { Dialog } from "../../shared/components/dialog/Dialog.tsx"
import { useTranslate } from "../../shared/hooks/translate.ts"
import { useAppDispatch, useAppSelector } from "../hooks/redux.ts"
import { dismissAlert, selectCurrentAlert } from "../slices/alertsSlice.ts"
/**
* Displays the current alert, if any.
*/
export const Alerts: FC = () => {
const dispatch = useAppDispatch()
const translate = useTranslate()
const currentAlert = useAppSelector(selectCurrentAlert)
const close = useCallback(() => {
dispatch(dismissAlert())
}, [dispatch])
if (currentAlert === undefined) {
return null
}
return (
<Dialog close={close} isOpen title={currentAlert.title} buttons={[{ label: translate("OK") }]}>
{currentAlert.description}
</Dialog>
)
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { classList } from "../../shared/utils/classList.ts"
import { ExternalAPI } from "../external.ts"
import { useAppSelector } from "../hooks/redux.ts"
import { selectAreAnimationsEnabled, selectLocale } from "../slices/settingsSlice.ts"
import { Alerts } from "./Alerts.tsx"
import { NavigationBar } from "./NavigationBar.tsx"
import "./Root.scss"
import { Router } from "./Router.tsx"
@@ -33,7 +34,7 @@ export const Root: FC = () => {
<img src={backgroundImg} alt="" />
</div>
{/* <AlertsContainer /> */}
<Alerts />
<TitleBar
platform={ExternalAPI.platform}
maximizeEvents={ExternalAPI}
@@ -0,0 +1,205 @@
import { UI } from "optolith-database-schema/types/UI"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { LocaleMap } from "optolith-database-schema/types/_LocaleMap"
import { ReactNode, memo, useCallback, useState } from "react"
import { Dropdown } from "../../../../../shared/components/dropdown/Dropdown.tsx"
import { DropdownOption } from "../../../../../shared/components/dropdown/DropdownItem.tsx"
import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx"
import { ListItem } from "../../../../../shared/components/list/ListItem.tsx"
import { ListItemButtons } from "../../../../../shared/components/list/ListItemButtons.tsx"
import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx"
import { ListItemLeft } from "../../../../../shared/components/list/ListItemLeft.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 {
DisplayedActiveActivatable,
nameChunkToString,
} from "../../../../../shared/domain/activatable/activatableActive.ts"
import { equalsIdentifier } from "../../../../../shared/domain/identifier.ts"
import { useTranslate } from "../../../../../shared/hooks/translate.ts"
import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts"
import { classList } from "../../../../../shared/utils/classList.ts"
import { range } from "../../../../../shared/utils/range.ts"
import { romanize } from "../../../../../shared/utils/roman.ts"
import { PickOfType } from "../../../../../shared/utils/type.ts"
import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import {
changeInlineLibraryEntry,
selectInlineLibraryEntryId,
} from "../../../../slices/inlineWikiSlice.ts"
type Props<T> = {
activatable: DisplayedActiveActivatable<T>
groupNameKey?: PickOfType<UI, string>
isCustomCostAvailable?: boolean
changeLevel: (
parameters: { id: number; index: number; level: number },
activeEntry: DisplayedActiveActivatable<T>,
) => boolean
remove: (
parameters: { id: number; index: number },
activeEntry: DisplayedActiveActivatable<T>,
) => void
createActivatableIdentifierObject: (id: number) => ActivatableIdentifier
}
const getLevelKey = (option: DropdownOption<number>) => option.id
const ActiveActivatablesListItem = <
T extends {
id: number
levels?: number
translations: LocaleMap<{ name: string }>
},
>(
props: Props<T>,
): ReactNode => {
const {
activatable,
groupNameKey,
isCustomCostAvailable = false,
changeLevel,
remove,
createActivatableIdentifierObject,
} = props
const {
static: { id, levels },
dynamic,
instanceIndex,
isRemovable,
cost,
name,
minLevel,
maxLevel,
} = activatable
const translate = useTranslate()
const translateMap = useTranslateMap()
const dispatch = useAppDispatch()
const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId)
const [selectedLevel, setSelectedLevel] = useState(dynamic.instances[instanceIndex]!.level)
const handleLevel = useCallback(
(level: number) => {
if (changeLevel({ id, index: instanceIndex, level }, activatable)) {
setSelectedLevel(level)
}
},
[activatable, changeLevel, id, instanceIndex],
)
const identifierObject = createActivatableIdentifierObject(id)
const handleSelectForInfo = useCallback(
() => dispatch(changeInlineLibraryEntry(identifierObject)),
[dispatch, identifierObject],
)
const handleRemove = useCallback(() => {
remove({ id, index: instanceIndex }, activatable)
}, [activatable, id, instanceIndex, remove])
const levelElement =
levels === undefined || selectedLevel === undefined || maxLevel === minLevel ? null : (
<Dropdown
className="level"
options={range([minLevel ?? 1, maxLevel ?? levels]).map(l => ({
id: l,
name: romanize(l),
}))}
getKey={getLevelKey}
value={selectedLevel}
onChange={handleLevel}
/>
)
const costStr = (() => {
// TODO
// renderMaybeWith((x: number | string) => (IAA.isAutomatic(item) ? `(${x})` : x))(
// PABSA.currentCost(snd(finalProps)),
// )
switch (cost.kind) {
case "fix":
return cost.fix.toString()
case "variable":
return `${cost.range[0]}${cost.range[1]}`
default:
return assertExhaustive(cost)
}
})()
return (
<ListItem
active={
inlineLibraryEntryId !== undefined &&
equalsIdentifier(inlineLibraryEntryId, identifierObject)
}
>
<ListItemLeft>
<ListItemName
name={nameChunkToString(
levelElement === null
? name.full
: Array.isArray(name.fullWithoutLevel)
? name.fullWithoutLevel[0]
: name.fullWithoutLevel,
translate,
translateMap,
)}
onClick={handleSelectForInfo}
/>
{levelElement}
{levelElement !== null && Array.isArray(name.fullWithoutLevel) ? (
<ListItemName
name={nameChunkToString(name.fullWithoutLevel[1], translate, translateMap)}
onClick={handleSelectForInfo}
/>
) : null}
</ListItemLeft>
<ListItemSeparator />
{groupNameKey === undefined ? null : <ListItemGroup text={translate(groupNameKey)} />}
<ListItemValues>
{isCustomCostAvailable ? (
<button
className={classList("cost", {
"custom-cost":
dynamic.instances[instanceIndex]!.customAdventurePointsValue !== undefined,
})}
>
{costStr}
</button>
) : (
<div className="cost">{costStr}</div>
)}
</ListItemValues>
<ListItemButtons>
<IconButton
icon="&#xE90b;"
disabled={!isRemovable}
onClick={handleRemove}
flat
label={translate("Remove")}
/>
<IconButton
icon="&#xE912;"
onClick={handleSelectForInfo}
flat
label={translate("Show details")}
/>
</ListItemButtons>
</ListItem>
)
}
/**
* Displays an activatable entry that is currently active.
*/
const MemoActiveActivatablesListItem = memo(
ActiveActivatablesListItem,
) as typeof ActiveActivatablesListItem
export { MemoActiveActivatablesListItem as ActiveActivatablesListItem }
@@ -0,0 +1,294 @@
import { UI } from "optolith-database-schema/types/UI"
import { AdventurePointsValue } from "optolith-database-schema/types/_Activatable"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { LocaleMap } from "optolith-database-schema/types/_LocaleMap"
import { ReactNode, memo, useCallback, useMemo, useState } from "react"
import { BasicInputDialog } from "../../../../../shared/components/basicInputDialog/BasicInputDialog.tsx"
import { Dropdown } from "../../../../../shared/components/dropdown/Dropdown.tsx"
import { DropdownOption } from "../../../../../shared/components/dropdown/DropdownItem.tsx"
import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx"
import { ListItem } from "../../../../../shared/components/list/ListItem.tsx"
import { ListItemButtons } from "../../../../../shared/components/list/ListItemButtons.tsx"
import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx"
import { ListItemLeft } from "../../../../../shared/components/list/ListItemLeft.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 { getCostOfInstance } from "../../../../../shared/domain/activatable/activatableActive.ts"
import {
ActivatableInstance,
ActivatableOption,
} from "../../../../../shared/domain/activatable/activatableEntry.ts"
import {
DisplayedInactiveActivatable,
getSelectOptionFromDisplayedOption,
} from "../../../../../shared/domain/activatable/activatableInactive.ts"
import { AdvantageIdentifier, equalsIdentifier } from "../../../../../shared/domain/identifier.ts"
import { useTranslate } from "../../../../../shared/hooks/translate.ts"
import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts"
import { classList } from "../../../../../shared/utils/classList.ts"
import { parseInt } from "../../../../../shared/utils/math.ts"
import { ensure, isNotNullish } from "../../../../../shared/utils/nullable.ts"
import { range } from "../../../../../shared/utils/range.ts"
import { romanize } from "../../../../../shared/utils/roman.ts"
import { isNonEmptyString } from "../../../../../shared/utils/string.ts"
import { PickOfType } from "../../../../../shared/utils/type.ts"
import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts"
import { useModalState } from "../../../../hooks/modalState.ts"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import {
changeInlineLibraryEntry,
selectInlineLibraryEntryId,
} from "../../../../slices/inlineWikiSlice.ts"
import { InactiveActivatableListItemOption } from "./InactiveActivatableListItemOption.tsx"
type Props<T> = {
activatable: DisplayedInactiveActivatable<T>
groupNameKey?: PickOfType<UI, string>
isCustomCostAvailable?: boolean
add: (parameters: { id: number; instance: ActivatableInstance }, cost: number) => void
createActivatableIdentifierObject: (id: number) => ActivatableIdentifier
}
const getLevelKey = (option: DropdownOption<number>) => option.id
const InactiveActivatablesListItem = <
T extends {
id: number
ap_value: AdventurePointsValue
translations: LocaleMap<{ name: string }>
},
>(
props: Props<T>,
): ReactNode => {
const {
activatable: {
static: { id, ap_value, translations },
dynamic,
isAvailable,
options,
level,
},
groupNameKey,
isCustomCostAvailable = false,
add,
createActivatableIdentifierObject,
} = props
const translate = useTranslate()
const translateMap = useTranslateMap()
const dispatch = useAppDispatch()
const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId)
const { name = "" } = translateMap(translations) ?? {}
const [selectedLevel, setSelectedLevel] = useState(() => (level === undefined ? undefined : 1))
const [selectedOptions, setSelectedOptions] = useState<(ActivatableOption | undefined)[]>([])
const [customCost, setCustomCost] = useState<number | undefined>()
const [customCostPreview, setCustomCostPreview] = useState<string>()
const {
isOpen: showCustomCostDialog,
open: openCustomCostDialog,
close: closeCustomCostDialog,
} = useModalState()
const handleChangeOption = useCallback((option: ActivatableOption | undefined, index: number) => {
setSelectedOptions(oldSelectedOptions => {
const newSelectedOptions = [...oldSelectedOptions]
// TODO: Reset options with higher indices?
// const newSelectedOptions = oldSelectedOptions.slice(0, index)
newSelectedOptions[index] = option
return newSelectedOptions
})
}, [])
const handleChangeLevel = useCallback((newLevel: number) => {
setSelectedLevel(newLevel)
// TODO: Reset options for specific entries?
// if (DisadvantageIdentifier.Principles === id || DisadvantageIdentifier.Obligations === id) {
// setSelectedOptions([])
// }
}, [])
const handleShowCustomCostDialog = useCallback(() => {
if (isCustomCostAvailable) {
openCustomCostDialog()
setCustomCostPreview(customCost?.toString())
}
}, [isCustomCostAvailable, openCustomCostDialog, customCost])
const handleSetCustomCost = useCallback(
() => setCustomCost(customCostPreview === undefined ? undefined : parseInt(customCostPreview)),
[customCostPreview],
)
const handleSetCustomCostPreview = useCallback(
(newCustomCostPreview: string) =>
setCustomCostPreview(ensure(newCustomCostPreview, isNonEmptyString)),
[setCustomCostPreview],
)
const handleDeleteCustomCost = useCallback(() => setCustomCost(undefined), [setCustomCost])
const idObject = useMemo(
() => createActivatableIdentifierObject(id),
[createActivatableIdentifierObject, id],
)
const handleSelectForInfo = useCallback(
() => dispatch(changeInlineLibraryEntry(idObject)),
[dispatch, idObject],
)
const cost = useMemo(
() =>
getCostOfInstance(
idObject,
ap_value,
dynamic?.instances ?? [],
{
level: selectedLevel,
options: selectedOptions.filter(isNotNullish),
customAdventurePointsValue: customCost,
},
optionId => getSelectOptionFromDisplayedOption(options, optionId),
true,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[ap_value, customCost, options, selectedLevel, selectedOptions],
)
const handleAdd = useCallback(() => {
add(
{
id,
instance: {
level: selectedLevel,
options: selectedOptions.filter(isNotNullish),
customAdventurePointsValue: customCost,
},
},
cost ?? 0,
)
setSelectedLevel(undefined)
setSelectedOptions([])
}, [add, cost, customCost, id, selectedLevel, selectedOptions])
if (equalsIdentifier({ tag: "Advantage", advantage: AdvantageIdentifier.HatredOf }, idObject)) {
console.log("cost", cost)
console.log("options", options)
}
const isAddDisabled = useMemo(
() =>
!isAvailable ||
selectedOptions.filter(isNotNullish).length !==
options.reduce((optionsSum, option) => {
switch (option.kind) {
case "choice":
return optionsSum + (option.count ?? 1)
case "choiceOrCustomizableText":
case "text":
return optionsSum + 1
default:
return assertExhaustive(option)
}
}, 0) ||
(level === undefined) !== (selectedLevel === undefined) ||
cost === undefined,
[cost, isAvailable, level, options, selectedLevel, selectedOptions],
)
const levelElement =
level === undefined || !isAvailable ? null : level.maximum > 1 ? (
<Dropdown
className="level"
options={range([1, level.maximum]).map(l => ({ id: l, name: romanize(l) }))}
getKey={getLevelKey}
value={selectedLevel}
onChange={handleChangeLevel}
/>
) : (
<span className="level title">{romanize(1)}</span>
)
return (
<ListItem
active={
inlineLibraryEntryId !== undefined && equalsIdentifier(inlineLibraryEntryId, idObject)
}
disabled={!isAvailable}
>
<ListItemLeft>
<ListItemName name={name} onClick={handleSelectForInfo} />
{level?.placement === "after" ? levelElement : null}
{isAvailable
? options.map((option, index) => (
<InactiveActivatableListItemOption
// eslint-disable-next-line react/no-array-index-key
key={index}
index={index}
onChange={handleChangeOption}
option={option}
value={selectedOptions[index]}
/>
))
: null}
{level?.placement === "before" ? levelElement : null}
</ListItemLeft>
<ListItemSeparator />
{groupNameKey === undefined ? null : <ListItemGroup text={translate(groupNameKey)} />}
<ListItemValues>
{isCustomCostAvailable ? (
<button
className={classList("cost", {
"custom-cost": customCost !== undefined,
})}
onClick={handleShowCustomCostDialog}
>
{cost ?? null}
</button>
) : (
<div className="cost">{cost ?? null}</div>
)}
<BasicInputDialog
id="custom-cost-dialog"
isOpen={showCustomCostDialog}
title={translate("Custom AP Cost")}
description={translate("AP Cost for {0}", name)}
value={customCostPreview}
acceptLabel={translate("Done")}
rejectLabel={translate("Delete")}
rejectDisabled={customCost === undefined}
onClose={closeCustomCostDialog}
onAccept={handleSetCustomCost}
onReject={handleDeleteCustomCost}
onChange={handleSetCustomCostPreview}
/>
</ListItemValues>
<ListItemButtons>
<IconButton
icon="&#xE916;"
disabled={isAddDisabled}
onClick={handleAdd}
flat
label={translate("Add")}
/>
<IconButton
icon="&#xE912;"
onClick={handleSelectForInfo}
flat
label={translate("Show details")}
/>
</ListItemButtons>
</ListItem>
)
}
/**
* Displays an activatable entry that is currently inactive.
*/
const MemoInactiveActivatablesListItem = memo(InactiveActivatablesListItem)
export { MemoInactiveActivatablesListItem as InactiveActivatablesListItem }
@@ -0,0 +1,101 @@
import { SelectOptionIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { FC, useCallback } from "react"
import { Dropdown } from "../../../../../shared/components/dropdown/Dropdown.tsx"
import { DropdownOption } from "../../../../../shared/components/dropdown/DropdownItem.tsx"
import { TextField } from "../../../../../shared/components/textField/TextField.tsx"
import { ActivatableOption } from "../../../../../shared/domain/activatable/activatableEntry.ts"
import { DisplayedInactiveActivatableOption } from "../../../../../shared/domain/activatable/activatableInactive.ts"
import { identifierObjectToString } from "../../../../../shared/domain/identifier.ts"
import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts"
import { useTranslate } from "../../../../../shared/hooks/translate.ts"
import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts"
import { compareAt } from "../../../../../shared/utils/compare.ts"
import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts"
type Props = {
option: DisplayedInactiveActivatableOption
value: ActivatableOption | undefined
onChange: (option: ActivatableOption | undefined, index: number) => void
index: number
}
const getKey = (option: DropdownOption<SelectOptionIdentifier | undefined>) =>
option.id === undefined ? "default" : identifierObjectToString(option.id)
/**
* Displays controls for handling a single option of an activatable entry.
*/
export const InactiveActivatableListItemOption: FC<Props> = props => {
const { option, value, onChange, index } = props
const translate = useTranslate()
const translateMap = useTranslateMap()
const localeCompare = useLocaleCompare()
const handleDropdownChange = useCallback(
(id: SelectOptionIdentifier) => {
onChange({ type: "Predefined", id }, index)
},
[index, onChange],
)
const handleTextfieldChange = useCallback(
(newText: string) => {
onChange(newText === "" ? undefined : { type: "Custom", value: newText }, index)
},
[index, onChange],
)
switch (option.kind) {
case "choice":
return (
<Dropdown
options={option.options
.map(o => ({
id: o.id,
name: translateMap(o.translations)?.name ?? "…",
}))
.toSorted(compareAt(opt => opt.name, localeCompare))}
value={value?.type === "Custom" ? undefined : value?.id}
onChange={handleDropdownChange}
getKey={getKey}
/>
)
case "choiceOrCustomizableText":
return (
<>
<Dropdown
key={index}
options={
value?.type === "Custom"
? [{ id: undefined, name: translate("Custom option") }]
: option.options
.map(o => ({
id: o.id,
name: translateMap(o.translations)?.name ?? "…",
}))
.toSorted(compareAt(opt => opt.name, localeCompare))
}
value={value?.type === "Custom" ? undefined : value?.id}
disabled={value?.type === "Custom"}
onChange={handleDropdownChange}
getKey={getKey}
/>
<TextField
onChange={handleTextfieldChange}
value={value?.type === "Custom" ? value.value : ""}
hint={translateMap(option.textLabel) ?? translate("Custom option")}
/>
</>
)
case "text":
return (
<TextField
onChange={handleTextfieldChange}
value={value?.type === "Custom" ? value.value : ""}
hint={translateMap(option.textLabel)}
/>
)
default:
return assertExhaustive(option)
}
}
@@ -0,0 +1,32 @@
import { FC } from "react"
import { DisplayedActiveAdvantage } from "../../../../../shared/domain/activatable/advantagesActive.ts"
import { getCreateIdentifierObject } from "../../../../../shared/domain/identifier.ts"
import { useActiveAdvantageAndDisadvantageActions } from "../../../../hooks/activatableActions.ts"
import { changeAdvantageLevel, removeAdvantage } from "../../../../slices/advantagesSlice.ts"
import { ActiveActivatablesListItem } from "../activatable/ActiveActivatableListItem.tsx"
type Props = {
advantage: DisplayedActiveAdvantage
}
/**
* Displays an advantage that is currently active.
*/
export const ActiveAdvantagesListItem: FC<Props> = props => {
const { advantage } = props
const { handleChangeLevel, handleRemove } = useActiveAdvantageAndDisadvantageActions(
"advantage",
removeAdvantage,
changeAdvantageLevel,
getCreateIdentifierObject("Advantage"),
)
return (
<ActiveActivatablesListItem
activatable={advantage}
changeLevel={handleChangeLevel}
remove={handleRemove}
createActivatableIdentifierObject={getCreateIdentifierObject("Advantage")}
/>
)
}
@@ -0,0 +1,35 @@
import { FC } from "react"
import { DisplayedActiveDisadvantage } from "../../../../../shared/domain/activatable/disadvantagesActive.ts"
import { getCreateIdentifierObject } from "../../../../../shared/domain/identifier.ts"
import { useActiveAdvantageAndDisadvantageActions } from "../../../../hooks/activatableActions.ts"
import {
changeDisadvantageLevel,
removeDisadvantage,
} from "../../../../slices/disadvantagesSlice.ts"
import { ActiveActivatablesListItem } from "../activatable/ActiveActivatableListItem.tsx"
type Props = {
disadvantage: DisplayedActiveDisadvantage
}
/**
* Displays a disadvantage that is currently active.
*/
export const ActiveDisadvantagesListItem: FC<Props> = props => {
const { disadvantage } = props
const { handleChangeLevel, handleRemove } = useActiveAdvantageAndDisadvantageActions(
"disadvantage",
removeDisadvantage,
changeDisadvantageLevel,
getCreateIdentifierObject("Disadvantage"),
)
return (
<ActiveActivatablesListItem
activatable={disadvantage}
changeLevel={handleChangeLevel}
remove={handleRemove}
createActivatableIdentifierObject={getCreateIdentifierObject("Disadvantage")}
/>
)
}
@@ -1,7 +1,8 @@
import { FC, useCallback, useState } from "react"
import { FC, useCallback, useMemo, useState } from "react"
import { Button } from "../../../../../shared/components/button/Button.tsx"
import { Checkbox } from "../../../../../shared/components/checkbox/Checkbox.tsx"
import { Grid } from "../../../../../shared/components/grid/Grid.tsx"
import { List } from "../../../../../shared/components/list/List.tsx"
import { ListHeader } from "../../../../../shared/components/list/ListHeader.tsx"
import { ListHeaderTag } from "../../../../../shared/components/list/ListHeaderTag.tsx"
import { ListPlaceholder } from "../../../../../shared/components/list/ListPlaceholder.tsx"
@@ -10,10 +11,19 @@ import { Options } from "../../../../../shared/components/options/Options.tsx"
import { Page } from "../../../../../shared/components/page/Page.tsx"
import { RecommendedReference } from "../../../../../shared/components/recommendedReference/RecommendedReference.tsx"
import { Scroll } from "../../../../../shared/components/scroll/Scroll.tsx"
import { Slidein } from "../../../../../shared/components/slidein/Slidein.tsx"
import { TextField } from "../../../../../shared/components/textField/TextField.tsx"
import { filterAndSortDisplayed } from "../../../../../shared/domain/activatable/advantagesDisadvantages.ts"
import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts"
import { useTranslate } from "../../../../../shared/hooks/translate.ts"
import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts"
import { useModalState } from "../../../../hooks/modalState.ts"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { InlineLibrary } from "../../../../inlineLibrary/InlineLibrary.tsx"
import {
selectVisibleActiveAdvantages,
selectVisibleInactiveAdvantages,
} from "../../../../selectors/advantageDisadvantageSelectors.ts"
import {
selectCanRemove,
selectIsInCharacterCreation,
@@ -22,19 +32,23 @@ import {
selectAdvantagesDisadvantagesCultureRatingVisibility,
switchAdvantagesDisadvantagesCultureRatingVisibility,
} from "../../../../slices/settingsSlice.ts"
import { ActiveAdvantagesListItem } from "./ActiveAdvantagesListItem.tsx"
import { AdventurePointsSpent } from "./AdventurePointsSpent.tsx"
import { InactiveAdvantagesListItem } from "./InactiveAdvantagesListItem.tsx"
/**
* Returns a page for managing advantages.
*/
export const Advantages: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
const dispatch = useAppDispatch()
const localeCompare = useLocaleCompare()
const cultureRatingVisibility = useAppSelector(
selectAdvantagesDisadvantagesCultureRatingVisibility,
)
const handlSwitchCultureRatingVisibility = useCallback(
const handleSwitchCultureRatingVisibility = useCallback(
() => dispatch(switchAdvantagesDisadvantagesCultureRatingVisibility()),
[dispatch],
)
@@ -42,16 +56,92 @@ export const Advantages: FC = () => {
const canRemove = useAppSelector(selectCanRemove)
const isInCharacterCreation = useAppSelector(selectIsInCharacterCreation)
const [filterText, setFilterText] = useState("")
const [activeFilterText, setActiveFilterText] = useState("")
const [inactiveFilterText, setInactiveFilterText] = useState("")
const visibleInactiveAdvantages = useAppSelector(selectVisibleInactiveAdvantages)
const visibleActiveAdvantages = useAppSelector(selectVisibleActiveAdvantages)
const inactiveList = useMemo(
() =>
filterAndSortDisplayed(
visibleInactiveAdvantages,
inactiveFilterText,
translateMap,
localeCompare,
),
[visibleInactiveAdvantages, inactiveFilterText, translateMap, localeCompare],
)
const activeList = useMemo(
() =>
filterAndSortDisplayed(
visibleActiveAdvantages,
activeFilterText,
translateMap,
localeCompare,
),
[visibleActiveAdvantages, activeFilterText, translateMap, localeCompare],
)
const { isOpen: isSlideinVisible, open: openSlidein, close: closeSlidein } = useModalState()
return (
<Page id="advantages">
<Slidein isOpen={isSlideinVisible} close={closeSlidein} className="adding-advantages">
<Options>
<TextField
value={inactiveFilterText}
onChange={setInactiveFilterText}
hint={translate("Search")}
/>
<Grid size="medium">
<Checkbox
checked={cultureRatingVisibility}
onClick={handleSwitchCultureRatingVisibility}
>
{translate("Common Advantages")}
</Checkbox>
{cultureRatingVisibility ? <RecommendedReference strongly /> : null}
</Grid>
{isInCharacterCreation && <AdventurePointsSpent />}
{/* <Checkbox checked={enableActiveItemHints} onClick={switchActiveItemHints}>
{translate(staticData)("general.filters.showactivatedentries")}
</Checkbox> */}
</Options>
<Main classOnly>
<ListHeader>
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="cost" hint={translate("Adventure Points")}>
{translate("AP")}
</ListHeaderTag>
<ListHeaderTag className="btn-placeholder" />
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll>
{inactiveList.length > 0 ? (
<List>
{inactiveList.map(x => (
<InactiveAdvantagesListItem key={`advantage-${x.static.id}`} advantage={x} />
))}
</List>
) : (
<ListPlaceholder type="inactiveAdvantages" message={translate("No Results")} />
)}
</Scroll>
</Main>
<InlineLibrary />
</Slidein>
<Options>
<TextField value={filterText} onChange={setFilterText} hint={translate("Search")} />
<Button>{translate("Add")}</Button>
<TextField
value={activeFilterText}
onChange={setActiveFilterText}
hint={translate("Search")}
/>
<Button onClick={openSlidein}>{translate("Add")}</Button>
<Grid size="medium">
<Checkbox checked={cultureRatingVisibility} onClick={handlSwitchCultureRatingVisibility}>
{translate("advantages.filters.commonadvantages")}
<Checkbox checked={cultureRatingVisibility} onClick={handleSwitchCultureRatingVisibility}>
{translate("Common Advantages")}
</Checkbox>
{cultureRatingVisibility ? <RecommendedReference strongly /> : null}
</Grid>
@@ -59,20 +149,23 @@ export const Advantages: FC = () => {
</Options>
<Main>
<ListHeader>
<ListHeaderTag className="name">
{translate("advantagesdisadvantages.header.name")}
</ListHeaderTag>
<ListHeaderTag
className="cost"
hint={translate("advantagesdisadvantages.header.adventurepoints.tooltip")}
>
{translate("advantagesdisadvantages.header.adventurepoints")}
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="cost" hint={translate("Adventure Points")}>
{translate("AP")}
</ListHeaderTag>
{canRemove && <ListHeaderTag className="btn-placeholder" />}
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll stable>
<ListPlaceholder type="advantages" message={translate("No Results")} />
{activeList.length > 0 ? (
<List>
{activeList.map(x => (
<ActiveAdvantagesListItem key={`advantage-${x.static.id}`} advantage={x} />
))}
</List>
) : (
<ListPlaceholder type="advantages" message={translate("No Results")} />
)}
</Scroll>
</Main>
<InlineLibrary />
@@ -66,25 +66,25 @@ export const AdventurePointsSpent: FC = () => {
<p>
{translate(
"{0}/{1} AP received from disadvantages",
spentOnDisadvantages.general + spentOnDisadvantages.bound,
Math.abs(spentOnDisadvantages.general + spentOnDisadvantages.bound),
80,
)}
{spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound > 0 ? (
{spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound < 0 ? (
<>
<br />
{translate(
"Thereof {0}/{1} from magic disadvantages",
spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound,
Math.abs(spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound),
maximumForMagicalAdvantagesDisadvantages,
)}
</>
) : null}
{spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound > 0 ? (
{spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound < 0 ? (
<>
<br />
{translate(
"Thereof {0}/{1} from blessed disadvantages",
spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound,
Math.abs(spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound),
50,
)}
</>
@@ -1,7 +1,8 @@
import { FC, useCallback, useState } from "react"
import { FC, useCallback, useMemo, useState } from "react"
import { Button } from "../../../../../shared/components/button/Button.tsx"
import { Checkbox } from "../../../../../shared/components/checkbox/Checkbox.tsx"
import { Grid } from "../../../../../shared/components/grid/Grid.tsx"
import { List } from "../../../../../shared/components/list/List.tsx"
import { ListHeader } from "../../../../../shared/components/list/ListHeader.tsx"
import { ListHeaderTag } from "../../../../../shared/components/list/ListHeaderTag.tsx"
import { ListPlaceholder } from "../../../../../shared/components/list/ListPlaceholder.tsx"
@@ -10,10 +11,19 @@ import { Options } from "../../../../../shared/components/options/Options.tsx"
import { Page } from "../../../../../shared/components/page/Page.tsx"
import { RecommendedReference } from "../../../../../shared/components/recommendedReference/RecommendedReference.tsx"
import { Scroll } from "../../../../../shared/components/scroll/Scroll.tsx"
import { Slidein } from "../../../../../shared/components/slidein/Slidein.tsx"
import { TextField } from "../../../../../shared/components/textField/TextField.tsx"
import { filterAndSortDisplayed } from "../../../../../shared/domain/activatable/advantagesDisadvantages.ts"
import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts"
import { useTranslate } from "../../../../../shared/hooks/translate.ts"
import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts"
import { useModalState } from "../../../../hooks/modalState.ts"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { InlineLibrary } from "../../../../inlineLibrary/InlineLibrary.tsx"
import {
selectVisibleActiveDisadvantages,
selectVisibleInactiveDisadvantages,
} from "../../../../selectors/advantageDisadvantageSelectors.ts"
import {
selectCanRemove,
selectIsInCharacterCreation,
@@ -22,19 +32,23 @@ import {
selectAdvantagesDisadvantagesCultureRatingVisibility,
switchAdvantagesDisadvantagesCultureRatingVisibility,
} from "../../../../slices/settingsSlice.ts"
import { ActiveDisadvantagesListItem } from "./ActiveDisadvantagesListItem.tsx"
import { AdventurePointsSpent } from "./AdventurePointsSpent.tsx"
import { InactiveDisadvantagesListItem } from "./InactiveDisadvantagesListItem.tsx"
/**
* Returns a page for managing disadvantages.
*/
export const Disadvantages: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
const dispatch = useAppDispatch()
const localeCompare = useLocaleCompare()
const cultureRatingVisibility = useAppSelector(
selectAdvantagesDisadvantagesCultureRatingVisibility,
)
const handlSwitchCultureRatingVisibility = useCallback(
const handleSwitchCultureRatingVisibility = useCallback(
() => dispatch(switchAdvantagesDisadvantagesCultureRatingVisibility()),
[dispatch],
)
@@ -42,16 +56,95 @@ export const Disadvantages: FC = () => {
const canRemove = useAppSelector(selectCanRemove)
const isInCharacterCreation = useAppSelector(selectIsInCharacterCreation)
const [filterText, setFilterText] = useState("")
const [activeFilterText, setActiveFilterText] = useState("")
const [inactiveFilterText, setInactiveFilterText] = useState("")
const visibleInactiveDisadvantages = useAppSelector(selectVisibleInactiveDisadvantages)
const visibleActiveDisadvantages = useAppSelector(selectVisibleActiveDisadvantages)
const inactiveList = useMemo(
() =>
filterAndSortDisplayed(
visibleInactiveDisadvantages,
inactiveFilterText,
translateMap,
localeCompare,
),
[visibleInactiveDisadvantages, inactiveFilterText, translateMap, localeCompare],
)
const activeList = useMemo(
() =>
filterAndSortDisplayed(
visibleActiveDisadvantages,
activeFilterText,
translateMap,
localeCompare,
),
[visibleActiveDisadvantages, activeFilterText, translateMap, localeCompare],
)
const { isOpen: isSlideinVisible, open: openSlidein, close: closeSlidein } = useModalState()
return (
<Page id="disadvantages">
<Slidein isOpen={isSlideinVisible} close={closeSlidein} className="adding-disadvantages">
<Options>
<TextField
value={inactiveFilterText}
onChange={setInactiveFilterText}
hint={translate("Search")}
/>
<Grid size="medium">
<Checkbox
checked={cultureRatingVisibility}
onClick={handleSwitchCultureRatingVisibility}
>
{translate("Common Disadvantages")}
</Checkbox>
{cultureRatingVisibility ? <RecommendedReference strongly /> : null}
</Grid>
{isInCharacterCreation && <AdventurePointsSpent />}
{/* <Checkbox checked={enableActiveItemHints} onClick={switchActiveItemHints}>
{translate(staticData)("general.filters.showactivatedentries")}
</Checkbox> */}
</Options>
<Main classOnly>
<ListHeader>
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="cost" hint={translate("Adventure Points")}>
{translate("AP")}
</ListHeaderTag>
<ListHeaderTag className="btn-placeholder" />
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll>
{inactiveList.length > 0 ? (
<List>
{inactiveList.map(x => (
<InactiveDisadvantagesListItem
key={`disadvantage-${x.static.id}`}
disadvantage={x}
/>
))}
</List>
) : (
<ListPlaceholder type="inactiveDisadvantages" message={translate("No Results")} />
)}
</Scroll>
</Main>
<InlineLibrary />
</Slidein>
<Options>
<TextField value={filterText} onChange={setFilterText} hint={translate("Search")} />
<Button>{translate("Add")}</Button>
<TextField
value={activeFilterText}
onChange={setActiveFilterText}
hint={translate("Search")}
/>
<Button onClick={openSlidein}>{translate("Add")}</Button>
<Grid size="medium">
<Checkbox checked={cultureRatingVisibility} onClick={handlSwitchCultureRatingVisibility}>
{translate("disadvantages.filters.commondisadvantages")}
<Checkbox checked={cultureRatingVisibility} onClick={handleSwitchCultureRatingVisibility}>
{translate("Common Disadvantages")}
</Checkbox>
{cultureRatingVisibility ? <RecommendedReference strongly /> : null}
</Grid>
@@ -59,20 +152,23 @@ export const Disadvantages: FC = () => {
</Options>
<Main>
<ListHeader>
<ListHeaderTag className="name">
{translate("advantagesdisadvantages.header.name")}
</ListHeaderTag>
<ListHeaderTag
className="cost"
hint={translate("advantagesdisadvantages.header.adventurepoints.tooltip")}
>
{translate("advantagesdisadvantages.header.adventurepoints")}
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="cost" hint={translate("Adventure Points")}>
{translate("AP")}
</ListHeaderTag>
{canRemove && <ListHeaderTag className="btn-placeholder" />}
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll stable>
<ListPlaceholder type="disadvantages" message={translate("No Results")} />
{activeList.length > 0 ? (
<List>
{activeList.map(x => (
<ActiveDisadvantagesListItem key={`disadvantage-${x.static.id}`} disadvantage={x} />
))}
</List>
) : (
<ListPlaceholder type="disadvantages" message={translate("No Results")} />
)}
</Scroll>
</Main>
<InlineLibrary />
@@ -0,0 +1,26 @@
import { FC } from "react"
import { DisplayedInactiveAdvantage } from "../../../../../shared/domain/activatable/advantagesInactive.ts"
import { getCreateIdentifierObject } from "../../../../../shared/domain/identifier.ts"
import { useInactiveAdvantageAndDisadvantageActions } from "../../../../hooks/activatableActions.ts"
import { addAdvantage } from "../../../../slices/advantagesSlice.ts"
import { InactiveActivatablesListItem } from "../activatable/InactiveActivatableListItem.tsx"
type Props = {
advantage: DisplayedInactiveAdvantage
}
/**
* Displays an advantage that is currently inactive.
*/
export const InactiveAdvantagesListItem: FC<Props> = props => {
const { advantage } = props
const { handleAdd } = useInactiveAdvantageAndDisadvantageActions("advantage", addAdvantage)
return (
<InactiveActivatablesListItem
activatable={advantage}
add={handleAdd}
createActivatableIdentifierObject={getCreateIdentifierObject("Advantage")}
/>
)
}
@@ -0,0 +1,26 @@
import { FC } from "react"
import { DisplayedInactiveDisadvantage } from "../../../../../shared/domain/activatable/disadvantagesInactive.ts"
import { getCreateIdentifierObject } from "../../../../../shared/domain/identifier.ts"
import { useInactiveAdvantageAndDisadvantageActions } from "../../../../hooks/activatableActions.ts"
import { addDisadvantage } from "../../../../slices/disadvantagesSlice.ts"
import { InactiveActivatablesListItem } from "../activatable/InactiveActivatableListItem.tsx"
type Props = {
disadvantage: DisplayedInactiveDisadvantage
}
/**
* Displays an disadvantage that is currently inactive.
*/
export const InactiveDisadvantagesListItem: FC<Props> = props => {
const { disadvantage } = props
const { handleAdd } = useInactiveAdvantageAndDisadvantageActions("disadvantage", addDisadvantage)
return (
<InactiveActivatablesListItem
activatable={disadvantage}
add={handleAdd}
createActivatableIdentifierObject={getCreateIdentifierObject("Disadvantage")}
/>
)
}
@@ -58,17 +58,10 @@ export const SpecialAbilities: FC = () => {
</Options>
<Main>
<ListHeader>
<ListHeaderTag className="name">
{translate("specialabilities.header.name")}
</ListHeaderTag>
<ListHeaderTag className="group">
{translate("specialabilities.header.group")}
</ListHeaderTag>
<ListHeaderTag
className="cost"
hint={translate("specialabilities.header.adventurepoints.tooltip")}
>
{translate("specialabilities.header.adventurepoints")}
<ListHeaderTag className="name">{translate("Name")}</ListHeaderTag>
<ListHeaderTag className="group">{translate("Group")}</ListHeaderTag>
<ListHeaderTag className="cost" hint={translate("Adventure Points")}>
{translate("AP")}
</ListHeaderTag>
{canRemove ? <ListHeaderTag className="btn-placeholder" /> : null}
<ListHeaderTag className="btn-placeholder" />
@@ -0,0 +1,16 @@
import { createSelector } from "@reduxjs/toolkit"
import { ResolvedSelectOption } from "optolith-database-schema/cache/activatableSelectOptions"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { getSelectOptionsFromCacheById } from "../../shared/domain/database.ts"
import { selectDatabase } from "../slices/databaseSlice.ts"
/**
* Selects a function that returns the select options for a given activatable
*identifier.
*/
export const selectGetSelectOptionsById = createSelector(
selectDatabase,
database =>
(id: ActivatableIdentifier): ResolvedSelectOption[] | undefined =>
getSelectOptionsFromCacheById(database.cache.activatableSelectOptions, id),
)
@@ -0,0 +1,426 @@
import { createSelector } from "@reduxjs/toolkit"
import {
countActivations,
isTinyActivatableActive,
} from "../../shared/domain/activatable/activatableEntry.ts"
import {
DisplayedActiveAdvantage,
getActiveAdvantages,
} from "../../shared/domain/activatable/advantagesActive.ts"
import {
DisplayedInactiveAdvantage,
getInactiveAdvantages,
} from "../../shared/domain/activatable/advantagesInactive.ts"
import { getActiveDisadvantages } from "../../shared/domain/activatable/disadvantagesActive.ts"
import { getInactiveDisadvantages } from "../../shared/domain/activatable/disadvantagesInactive.ts"
import {
isRatedActive,
isRatedWithEnhancementsActive,
} from "../../shared/domain/rated/ratedEntry.ts"
import { count, sumWith } from "../../shared/utils/array.ts"
import { selectGetSelectOptionsById } from "./activatableSelectors.ts"
import { SelectAll, SelectGetById } from "./basicCapabilitySelectors.ts"
import { selectFilterApplyingActivatableDependencies } from "./dependencySelectors.ts"
import { selectStartExperienceLevel } from "./experienceLevelSelectors.ts"
import { selectCapabilitiesForPrerequisitesOfAdvantageOrDisadvantage } from "./prerequisiteSelectors.ts"
import { selectIsEntryAvailable } from "./publicationSelectors.ts"
/**
* Returns all all advantages with their corresponding dynamic entries,
* extended by whether the entry can be activated.
*/
export const selectVisibleInactiveAdvantages = createSelector(
SelectAll.Static.Advantages,
SelectGetById.Dynamic.Advantage,
selectIsEntryAvailable,
selectCapabilitiesForPrerequisitesOfAdvantageOrDisadvantage,
selectGetSelectOptionsById,
selectFilterApplyingActivatableDependencies,
createSelector(SelectAll.Dynamic.Sermons, dynamicSermons =>
sumWith(dynamicSermons, countActivations),
),
createSelector(SelectAll.Dynamic.Visions, dynamicVisions =>
sumWith(dynamicVisions, countActivations),
),
createSelector(
SelectAll.Dynamic.Spells,
SelectAll.Dynamic.Rituals,
(dynamicSpells, dynamicRituals) =>
count(dynamicSpells, isRatedWithEnhancementsActive) +
count(dynamicRituals, isRatedWithEnhancementsActive),
),
SelectGetById.Dynamic.Disadvantage,
(
staticAdvantages,
getDynamicAdvantageById,
isEntryAvailable,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
activeSermonsCount,
activeVisionsCount,
activeSpellworksCount,
getDynamicDisadvantageById,
): DisplayedInactiveAdvantage[] =>
getInactiveAdvantages(
staticAdvantages,
getDynamicAdvantageById,
isEntryAvailable,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
{
activeSermonsCount,
activeVisionsCount,
activeSpellworksCount,
getDynamicAdvantageById,
getDynamicDisadvantageById,
},
),
)
/**
* Returns all all disadvantages with their corresponding dynamic entries,
* extended by whether the entry can be activated.
*/
export const selectVisibleInactiveDisadvantages = createSelector(
SelectAll.Static.Disadvantages,
SelectGetById.Dynamic.Disadvantage,
selectIsEntryAvailable,
selectCapabilitiesForPrerequisitesOfAdvantageOrDisadvantage,
selectGetSelectOptionsById,
selectFilterApplyingActivatableDependencies,
createSelector(SelectAll.Dynamic.Sermons, dynamicSermons =>
sumWith(dynamicSermons, countActivations),
),
createSelector(SelectAll.Dynamic.Visions, dynamicVisions =>
sumWith(dynamicVisions, countActivations),
),
createSelector(
SelectAll.Dynamic.Spells,
SelectAll.Dynamic.Rituals,
(dynamicSpells, dynamicRituals) =>
count(dynamicSpells, isRatedWithEnhancementsActive) +
count(dynamicRituals, isRatedWithEnhancementsActive),
),
SelectGetById.Dynamic.Advantage,
(
staticDisadvantages,
getDynamicDisadvantageById,
isEntryAvailable,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
activeSermonsCount,
activeVisionsCount,
activeSpellworksCount,
getDynamicAdvantageById,
): DisplayedInactiveAdvantage[] =>
getInactiveDisadvantages(
staticDisadvantages,
getDynamicDisadvantageById,
isEntryAvailable,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
{
activeSermonsCount,
activeVisionsCount,
activeSpellworksCount,
getDynamicAdvantageById,
getDynamicDisadvantageById,
},
),
)
/**
* Returns all all advantages with their corresponding dynamic entries,
* extended by which activation/instance of the entry it represents.
*/
export const selectVisibleActiveAdvantages = createSelector(
SelectGetById.Static.Advantage,
SelectAll.Dynamic.Advantages,
selectCapabilitiesForPrerequisitesOfAdvantageOrDisadvantage,
selectGetSelectOptionsById,
selectFilterApplyingActivatableDependencies,
createSelector(SelectAll.Dynamic.Sermons, dynamicSermons =>
sumWith(dynamicSermons, countActivations),
),
createSelector(SelectAll.Dynamic.Visions, dynamicVisions =>
sumWith(dynamicVisions, countActivations),
),
createSelector(SelectAll.Dynamic.Cantrips, dynamicCantrips =>
count(dynamicCantrips, isTinyActivatableActive),
),
createSelector(
SelectAll.Dynamic.Spells,
SelectAll.Dynamic.Rituals,
(dynamicSpells, dynamicRituals) =>
count(dynamicSpells, isRatedWithEnhancementsActive) +
count(dynamicRituals, isRatedWithEnhancementsActive),
),
createSelector(
SelectAll.Dynamic.Curses,
SelectAll.Dynamic.ElvenMagicalSongs,
SelectAll.Dynamic.DominationRituals,
SelectAll.Dynamic.MagicalDances,
SelectAll.Dynamic.MagicalMelodies,
SelectAll.Dynamic.JesterTricks,
SelectAll.Dynamic.AnimistPowers,
SelectAll.Dynamic.GeodeRituals,
SelectAll.Dynamic.ZibiljaRituals,
(
dynamicCurses,
dynamicElvenMagicalSongs,
dynamicDominationRituals,
dynamicMagicalDances,
dynamicMagicalMelodies,
dynamicJesterTricks,
dynamicAnimistPowers,
dynamicGeodeRituals,
dynamicZibiljaRituals,
) =>
count(dynamicCurses, isRatedActive) +
count(dynamicElvenMagicalSongs, isRatedActive) +
count(dynamicDominationRituals, isRatedActive) +
count(dynamicMagicalDances, isRatedActive) +
count(dynamicMagicalMelodies, isRatedActive) +
count(dynamicJesterTricks, isRatedActive) +
count(dynamicAnimistPowers, isRatedActive) +
count(dynamicGeodeRituals, isRatedActive) +
count(dynamicZibiljaRituals, isRatedActive),
),
createSelector(SelectAll.Dynamic.Blessings, dynamicBlessings =>
count(dynamicBlessings, isTinyActivatableActive),
),
createSelector(SelectAll.Dynamic.LiturgicalChants, dynamicLiturgicalChants =>
count(dynamicLiturgicalChants, isRatedWithEnhancementsActive),
),
createSelector(SelectAll.Dynamic.Ceremonies, dynamicCeremonies =>
count(dynamicCeremonies, isRatedWithEnhancementsActive),
),
SelectGetById.Dynamic.Skill,
SelectGetById.Dynamic.LiturgicalChant,
SelectGetById.Dynamic.Ceremony,
SelectGetById.Dynamic.Spell,
SelectGetById.Dynamic.Ritual,
SelectGetById.Dynamic.CloseCombatTechnique,
SelectGetById.Dynamic.RangedCombatTechnique,
selectStartExperienceLevel,
(
getStaticAdvantageById,
dynamicAdvantages,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
activeSermonsCount,
activeVisionsCount,
activeCantripsCount,
activeSpellworksCount,
activeMagicalActionsCount,
activeBlessingsCount,
activeLiturgicalChantsCount,
activeCeremoniesCount,
getDynamicSkillById,
getDynamicLiturgicalChantById,
getDynamicCeremonyById,
getDynamicSpellById,
getDynamicRitualById,
getDynamicCloseCombatTechniqueById,
getDynamicRangedCombatTechniqueById,
startExperienceLevel,
): DisplayedActiveAdvantage[] =>
getActiveAdvantages(
getStaticAdvantageById,
dynamicAdvantages,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
{
startExperienceLevel,
activeSermonsCount,
activeVisionsCount,
activeCantripsCount,
activeSpellworksCount,
activeMagicalActionsCount,
activeBlessingsCount,
activeLiturgicalChantsCount,
activeCeremoniesCount,
getDynamicSkillById,
getDynamicLiturgicalChantById,
getDynamicCeremonyById,
getDynamicSpellById,
getDynamicRitualById,
getDynamicCloseCombatTechniqueById,
getDynamicRangedCombatTechniqueById,
},
),
)
/**
* Returns all all disadvantages with their corresponding dynamic entries,
* extended by which activation/instance of the entry it represents.
*/
export const selectVisibleActiveDisadvantages = createSelector(
SelectGetById.Static.Disadvantage,
SelectAll.Dynamic.Disadvantages,
selectCapabilitiesForPrerequisitesOfAdvantageOrDisadvantage,
selectGetSelectOptionsById,
selectFilterApplyingActivatableDependencies,
createSelector(SelectAll.Dynamic.Sermons, dynamicSermons =>
sumWith(dynamicSermons, countActivations),
),
createSelector(SelectAll.Dynamic.Visions, dynamicVisions =>
sumWith(dynamicVisions, countActivations),
),
createSelector(SelectAll.Dynamic.Cantrips, dynamicCantrips =>
count(dynamicCantrips, isTinyActivatableActive),
),
createSelector(
SelectAll.Dynamic.Spells,
SelectAll.Dynamic.Rituals,
(dynamicSpells, dynamicRituals) =>
count(dynamicSpells, isRatedWithEnhancementsActive) +
count(dynamicRituals, isRatedWithEnhancementsActive),
),
createSelector(
SelectAll.Dynamic.Curses,
SelectAll.Dynamic.ElvenMagicalSongs,
SelectAll.Dynamic.DominationRituals,
SelectAll.Dynamic.MagicalDances,
SelectAll.Dynamic.MagicalMelodies,
SelectAll.Dynamic.JesterTricks,
SelectAll.Dynamic.AnimistPowers,
SelectAll.Dynamic.GeodeRituals,
SelectAll.Dynamic.ZibiljaRituals,
(
dynamicCurses,
dynamicElvenMagicalSongs,
dynamicDominationRituals,
dynamicMagicalDances,
dynamicMagicalMelodies,
dynamicJesterTricks,
dynamicAnimistPowers,
dynamicGeodeRituals,
dynamicZibiljaRituals,
) =>
count(dynamicCurses, isRatedActive) +
count(dynamicElvenMagicalSongs, isRatedActive) +
count(dynamicDominationRituals, isRatedActive) +
count(dynamicMagicalDances, isRatedActive) +
count(dynamicMagicalMelodies, isRatedActive) +
count(dynamicJesterTricks, isRatedActive) +
count(dynamicAnimistPowers, isRatedActive) +
count(dynamicGeodeRituals, isRatedActive) +
count(dynamicZibiljaRituals, isRatedActive),
),
createSelector(SelectAll.Dynamic.Blessings, dynamicBlessings =>
count(dynamicBlessings, isTinyActivatableActive),
),
createSelector(SelectAll.Dynamic.LiturgicalChants, dynamicLiturgicalChants =>
count(dynamicLiturgicalChants, isRatedWithEnhancementsActive),
),
createSelector(SelectAll.Dynamic.Ceremonies, dynamicCeremonies =>
count(dynamicCeremonies, isRatedWithEnhancementsActive),
),
SelectGetById.Dynamic.Skill,
SelectGetById.Dynamic.LiturgicalChant,
SelectGetById.Dynamic.Ceremony,
SelectGetById.Dynamic.Spell,
SelectGetById.Dynamic.Ritual,
SelectGetById.Dynamic.CloseCombatTechnique,
SelectGetById.Dynamic.RangedCombatTechnique,
selectStartExperienceLevel,
(
getStaticDisadvantageById,
dynamicDisadvantages,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
activeSermonsCount,
activeVisionsCount,
activeCantripsCount,
activeSpellworksCount,
activeMagicalActionsCount,
activeBlessingsCount,
activeLiturgicalChantsCount,
activeCeremoniesCount,
getDynamicSkillById,
getDynamicLiturgicalChantById,
getDynamicCeremonyById,
getDynamicSpellById,
getDynamicRitualById,
getDynamicCloseCombatTechniqueById,
getDynamicRangedCombatTechniqueById,
startExperienceLevel,
): DisplayedActiveAdvantage[] =>
getActiveDisadvantages(
getStaticDisadvantageById,
dynamicDisadvantages,
prerequisiteCapabilities,
getSelectOptionsById,
filterApplyingDependencies,
{
startExperienceLevel,
activeSermonsCount,
activeVisionsCount,
activeCantripsCount,
activeSpellworksCount,
activeMagicalActionsCount,
activeBlessingsCount,
activeLiturgicalChantsCount,
activeCeremoniesCount,
getDynamicSkillById,
getDynamicLiturgicalChantById,
getDynamicCeremonyById,
getDynamicSpellById,
getDynamicRitualById,
getDynamicCloseCombatTechniqueById,
getDynamicRangedCombatTechniqueById,
},
),
)
// export const getAdvantagesRating = createMaybeSelector (
// getRace,
// getCulture,
// getProfession,
// (mrace, mculture, mprofession) =>
// liftM3 ((r: Record<Race>) => (c: Record<Culture>) => (p: Record<Profession>) =>
// pipe_ (
// OrderedMap.empty as RatingMap,
// flip (foldr (insertRating (EntryRating.Common)))
// (Race.A.commonAdvantages (r)),
// flip (foldr (insertRating (EntryRating.Uncommon)))
// (Race.A.uncommonAdvantages (r)),
// flip (foldr (insertRating (EntryRating.Common)))
// (Culture.A.commonAdvantages (c)),
// flip (foldr (insertRating (EntryRating.Uncommon)))
// (Culture.A.uncommonAdvantages (c)),
// flip (foldr (insertRating (EntryRating.Common)))
// (Profession.A.suggestedAdvantages (p)),
// flip (foldr (insertRating (EntryRating.Uncommon)))
// (Profession.A.unsuitableAdvantages (p)),
// flip (foldr (insertRating (EntryRating.Essential)))
// (Race.A.stronglyRecommendedAdvantages (r))
// ))
// (mrace)
// (mculture)
// (mprofession)
// )
// export const getAdvantagesForSheet = createMaybeSelector (
// getActiveForView (Category.ADVANTAGES),
// ident
// )
// export const getAdvantagesForEditMap = getActiveForViewMap (Category.ADVANTAGES)
// export const getAdvantagesForEdit = mapCurrentHero (getAdvantagesForEditMap)
@@ -3,6 +3,11 @@ import {
TinyActivatable,
isTinyActivatableActive,
} from "../../shared/domain/activatable/activatableEntry.ts"
import {
AdventurePointsCache,
addAdventurePointsCaches,
emptyAdventurePointsCache,
} from "../../shared/domain/adventurePoints/cache.ts"
import {
ImprovementCost,
adventurePointsForRange,
@@ -12,11 +17,13 @@ import { count } from "../../shared/utils/array.ts"
import {
selectCurrentCharacter,
selectDerivedCharacteristics,
selectDynamicAdvantages,
selectDynamicAnimistPowers,
selectDynamicAttributes,
selectDynamicCeremonies,
selectDynamicCloseCombatTechniques,
selectDynamicCurses,
selectDynamicDisadvantages,
selectDynamicDominationRituals,
selectDynamicElvenMagicalSongs,
selectDynamicGeodeRituals,
@@ -27,21 +34,14 @@ import {
selectDynamicRangedCombatTechniques,
selectDynamicRituals,
selectDynamicSkills,
selectDynamicSpecialAbilities,
selectDynamicSpells,
selectDynamicZibiljaRituals,
selectTotalAdventurePoints,
} from "../slices/characterSlice.ts"
import { selectMagicalAndBlessedAdvantagesAndDisadvantagesCache } from "../slices/databaseSlice.ts"
import { SelectAll } from "./basicCapabilitySelectors.ts"
/**
* Adventure Points can either be spent from the main pool of adventure points
* or from the pool bound to a specific entry.
*/
export type SpentAdventurePoints = {
general: number
bound: number
}
const sumRatedMaps = (
...ratedMaps: (Record<number, { cachedAdventurePoints: RatedAdventurePointsCache }> | undefined)[]
): SpentAdventurePoints =>
@@ -125,60 +125,102 @@ export const selectAdventurePointsSpentOnBlessings = createSelector(
sumTinyActivatables,
)
const sumActivatableMaps = (
...activatableMaps: Record<number, { cachedAdventurePoints: AdventurePointsCache }>[]
): AdventurePointsCache =>
activatableMaps
.flatMap(activatableMap => Object.values(activatableMap ?? {}))
.reduce(
(acc, rated) => addAdventurePointsCaches(acc, rated.cachedAdventurePoints),
emptyAdventurePointsCache,
)
const sumActivatables = (
activatables: { cachedAdventurePoints: AdventurePointsCache }[],
): AdventurePointsCache =>
activatables.reduce(
(acc, rated) => addAdventurePointsCaches(acc, rated.cachedAdventurePoints),
emptyAdventurePointsCache,
)
/**
* Returns the adventure points spent on advantages.
*/
export const selectAdventurePointsSpentOnAdvantages = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
selectDynamicAdvantages,
(dynamicAdvantages): AdventurePointsCache => sumActivatableMaps(dynamicAdvantages ?? {}),
)
/**
* Returns the adventure points spent on magical advantages.
*/
export const selectAdventurePointsSpentOnMagicalAdvantages = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
SelectAll.Dynamic.Advantages,
selectMagicalAndBlessedAdvantagesAndDisadvantagesCache,
(dynamicAdvantages, cache): AdventurePointsCache =>
sumActivatables(
dynamicAdvantages.filter(({ id }) => cache.advantages.magical.ids.includes(id)),
),
)
/**
* Returns the adventure points spent on blessed advantages.
*/
export const selectAdventurePointsSpentOnBlessedAdvantages = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
SelectAll.Dynamic.Advantages,
selectMagicalAndBlessedAdvantagesAndDisadvantagesCache,
(dynamicAdvantages, cache): AdventurePointsCache =>
sumActivatables(
dynamicAdvantages.filter(({ id }) => cache.advantages.blessed.ids.includes(id)),
),
)
/**
* Returns the adventure points spent on disadvantages.
*/
export const selectAdventurePointsSpentOnDisadvantages = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
selectDynamicDisadvantages,
(dynamicDisadvantages): AdventurePointsCache => {
const { general, bound } = sumActivatableMaps(dynamicDisadvantages ?? {})
return { general: -general, bound: -bound }
},
)
/**
* Returns the adventure points spent on magical disadvantages.
*/
export const selectAdventurePointsSpentOnMagicalDisadvantages = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
SelectAll.Dynamic.Disadvantages,
selectMagicalAndBlessedAdvantagesAndDisadvantagesCache,
(dynamicDisadvantages, cache): AdventurePointsCache => {
const { general, bound } = sumActivatables(
dynamicDisadvantages.filter(({ id }) => cache.disadvantages.magical.ids.includes(id)),
)
return { general: -general, bound: -bound }
},
)
/**
* Returns the adventure points spent on blessed disadvantages.
*/
export const selectAdventurePointsSpentOnBlessedDisadvantages = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
SelectAll.Dynamic.Disadvantages,
selectMagicalAndBlessedAdvantagesAndDisadvantagesCache,
(dynamicDisadvantages, cache): AdventurePointsCache => {
const { general, bound } = sumActivatables(
dynamicDisadvantages.filter(({ id }) => cache.disadvantages.blessed.ids.includes(id)),
)
return { general: -general, bound: -bound }
},
)
/**
* Returns the adventure points spent on special abilities.
*/
export const selectAdventurePointsSpentOnSpecialAbilities = createSelector(
selectCurrentCharacter,
(): SpentAdventurePoints => ({ general: 0, bound: 0 }),
selectDynamicSpecialAbilities,
(dynamicSpecialAbilities): AdventurePointsCache =>
sumActivatableMaps(...Object.values(dynamicSpecialAbilities ?? {})),
)
/**
@@ -227,17 +269,13 @@ export const selectAdventurePointsSpent = createSelector(
selectAdventurePointsSpentOnCantrips,
selectAdventurePointsSpentOnBlessings,
selectAdventurePointsSpentOnAdvantages,
selectAdventurePointsSpentOnMagicalAdvantages,
selectAdventurePointsSpentOnBlessedAdvantages,
selectAdventurePointsSpentOnDisadvantages,
selectAdventurePointsSpentOnMagicalDisadvantages,
selectAdventurePointsSpentOnBlessedDisadvantages,
selectAdventurePointsSpentOnSpecialAbilities,
selectAdventurePointsSpentOnEnergies,
selectAdventurePointsSpentOnRace,
selectAdventurePointsSpentOnProfession,
(...spentCategories): SpentAdventurePoints =>
spentCategories.reduce<SpentAdventurePoints>(
(...spentCategories): AdventurePointsCache =>
spentCategories.reduce<AdventurePointsCache>(
(acc, spentCategory) => ({
general:
acc.general +
@@ -1,7 +1,7 @@
/* eslint-disable jsdoc/require-jsdoc */
/* eslint-disable @typescript-eslint/no-namespace */
import { createSelector } from "@reduxjs/toolkit"
import { All, GetById } from "../../shared/domain/getTypes.ts"
import { All, GetById, Singleton } from "../../shared/domain/getTypes.ts"
import {
selectDynamicAdvancedCombatSpecialAbilities,
selectDynamicAdvancedKarmaSpecialAbilities,
@@ -76,6 +76,8 @@ import {
selectDynamicWandEnchantments,
selectDynamicWeaponEnchantments,
selectDynamicZibiljaRituals,
selectPact,
selectSex,
} from "../slices/characterSlice.ts"
import { selectDatabase } from "../slices/databaseSlice.ts"
@@ -349,6 +351,14 @@ export namespace SelectGetById {
}
}
// prettier-ignore
export namespace SelectSingleton {
export namespace Dynamic {
export const Pact = createSelector(selectPact, (pact): Singleton.Dynamic.Pact => pact)
export const Sex = createSelector(selectSex, (sex): Singleton.Dynamic.Sex => sex)
}
}
// prettier-ignore
export namespace SelectAll {
export namespace Static {
@@ -1,6 +1,10 @@
import { createSelector } from "@reduxjs/toolkit"
import { filterApplyingRatedDependencies } from "../../shared/domain/dependencies/filterApplyingDependencies.ts"
import {
filterApplyingActivatableDependencies,
filterApplyingRatedDependencies,
} from "../../shared/domain/dependencies/filterApplyingDependencies.ts"
import { checkMultipleDisjunctionPartsAreValid } from "../../shared/domain/prerequisites/disjunctionPrerequisiteMultipartValidation.ts"
import { checkPreconditions } from "../../shared/domain/prerequisites/fullPrerequisiteValidationForType.ts"
import { selectDatabase } from "../slices/databaseSlice.ts"
import * as PrerequisiteCaps from "./prerequisiteSelectors.ts"
@@ -91,6 +95,7 @@ export const selectFilterApplyingRatedDependencies = createSelector(
getStaticMagicalTraditionById: id => database.magicalTraditions[id],
getStaticBlessedTraditionById: id => database.blessedTraditions[id],
getStaticPactGiftById: id => database.pactGifts[id],
getStaticVampiricGiftById: id => database.vampiricGifts[id],
getStaticSikaryanDrainSpecialAbilityById: id => database.sikaryanDrainSpecialAbilities[id],
getStaticLycantropicGiftById: id => database.lycantropicGifts[id],
getStaticSkillStyleSpecialAbilityById: id => database.skillStyleSpecialAbilities[id],
@@ -108,6 +113,7 @@ export const selectFilterApplyingRatedDependencies = createSelector(
getStaticChronicleEnchantmentById: id => database.chronicleEnchantments[id],
getStaticKrallenkettenzauberById: id => database.krallenkettenzauber[id],
getStaticTrinkhornzauberById: id => database.trinkhornzauber[id],
getStaticMagicalSignById: id => database.magicalSigns[id],
getStaticSpellById: id => database.spells[id],
getStaticRitualById: id => database.rituals[id],
getStaticLiturgicalChantById: id => database.liturgicalChants[id],
@@ -134,5 +140,145 @@ export const selectFilterApplyingRatedDependencies = createSelector(
...statePrerequisiteCapabilities,
...textPrerequisiteCapabilities,
}),
ratedMinimumNumberPrerequisiteCapabilities,
),
)
/**
* Selects a filter function to filter unapplying dependencies of activatable
* entries.
*/
export const selectFilterApplyingActivatableDependencies = createSelector(
selectDatabase,
PrerequisiteCaps.selectCapabilitiesForActivatablePrerequisite,
PrerequisiteCaps.selectCapabilitiesForAncestorBloodPrerequisite,
PrerequisiteCaps.selectCapabilitiesForBlessedTraditionPrerequisite,
PrerequisiteCaps.selectCapabilitiesForCommonSuggestedByRCPPrerequisite,
PrerequisiteCaps.selectCapabilitiesForCulturePrerequisite,
PrerequisiteCaps.selectCapabilitiesForExternalEnhancementPrerequisite,
PrerequisiteCaps.selectCapabilitiesForInfluencePrerequisite,
PrerequisiteCaps.selectCapabilitiesForMagicalTraditionPrerequisite,
PrerequisiteCaps.selectCapabilitiesForPactPrerequisite,
PrerequisiteCaps.selectCapabilitiesForPrimaryAttributePrerequisite,
PrerequisiteCaps.selectCapabilitiesForPublicationPrerequisite,
PrerequisiteCaps.selectCapabilitiesForRacePrerequisite,
PrerequisiteCaps.selectCapabilitiesForRatedMinimumNumberPrerequisite,
PrerequisiteCaps.selectCapabilitiesForRatedPrerequisite,
PrerequisiteCaps.selectCapabilitiesForRatedSumPrerequisite,
PrerequisiteCaps.selectCapabilitiesForRulePrerequisite,
PrerequisiteCaps.selectCapabilitiesForSexPrerequisite,
PrerequisiteCaps.selectCapabilitiesForSexualCharacteristicPrerequisite,
PrerequisiteCaps.selectCapabilitiesForSocialStatusPrerequisite,
PrerequisiteCaps.selectCapabilitiesForStatePrerequisite,
PrerequisiteCaps.selectCapabilitiesForTextPrerequisite,
PrerequisiteCaps.selectCapabilitiesForPrecondition,
(
database,
activatablePrerequisiteCapabilities,
ancestorBloodPrerequisiteCapabilities,
blessedTraditionPrerequisiteCapabilities,
commonSuggestedByRCPPrerequisiteCapabilities,
culturePrerequisiteCapabilities,
externalEnhancementPrerequisiteCapabilities,
influencePrerequisiteCapabilities,
magicalTraditionPrerequisiteCapabilities,
pactPrerequisiteCapabilities,
primaryAttributePrerequisiteCapabilities,
publicationPrerequisiteCapabilities,
racePrerequisiteCapabilities,
ratedMinimumNumberPrerequisiteCapabilities,
ratedPrerequisiteCapabilities,
ratedSumPrerequisiteCapabilities,
rulePrerequisiteCapabilities,
sexPrerequisiteCapabilities,
sexualCharacteristicPrerequisiteCapabilities,
socialStatusPrerequisiteCapabilities,
statePrerequisiteCapabilities,
textPrerequisiteCapabilities,
preconditionCapabilities,
) =>
filterApplyingActivatableDependencies(
checkMultipleDisjunctionPartsAreValid({
getStaticDisadvantageById: id => database.disadvantages[id],
getStaticGeneralSpecialAbilityById: id => database.generalSpecialAbilities[id],
getStaticFatePointSpecialAbilityById: id => database.fatePointSpecialAbilities[id],
getStaticCombatSpecialAbilityById: id => database.combatSpecialAbilities[id],
getStaticMagicalSpecialAbilityById: id => database.magicalSpecialAbilities[id],
getStaticStaffEnchantmentById: id => database.staffEnchantments[id],
getStaticFamiliarSpecialAbilityById: id => database.familiarSpecialAbilities[id],
getStaticKarmaSpecialAbilityById: id => database.karmaSpecialAbilities[id],
getStaticProtectiveWardingCircleSpecialAbilityById: id =>
database.protectiveWardingCircleSpecialAbilities[id],
getStaticCombatStyleSpecialAbilityById: id => database.combatStyleSpecialAbilities[id],
getStaticAdvancedCombatSpecialAbilityById: id =>
database.advancedCombatSpecialAbilities[id],
getStaticCommandSpecialAbilityById: id => database.commandSpecialAbilities[id],
getStaticMagicStyleSpecialAbilityById: id => database.magicStyleSpecialAbilities[id],
getStaticAdvancedMagicalSpecialAbilityById: id =>
database.advancedMagicalSpecialAbilities[id],
getStaticSpellSwordEnchantmentById: id => database.spellSwordEnchantments[id],
getStaticDaggerRitualById: id => database.daggerRituals[id],
getStaticInstrumentEnchantmentById: id => database.instrumentEnchantments[id],
getStaticAttireEnchantmentById: id => database.attireEnchantments[id],
getStaticOrbEnchantmentById: id => database.orbEnchantments[id],
getStaticWandEnchantmentById: id => database.wandEnchantments[id],
getStaticBrawlingSpecialAbilityById: id => database.brawlingSpecialAbilities[id],
getStaticAncestorGlyphById: id => database.ancestorGlyphs[id],
getStaticCeremonialItemSpecialAbilityById: id =>
database.ceremonialItemSpecialAbilities[id],
getStaticSermonById: id => database.sermons[id],
getStaticLiturgicalStyleSpecialAbilityById: id =>
database.liturgicalStyleSpecialAbilities[id],
getStaticAdvancedKarmaSpecialAbilityById: id => database.advancedKarmaSpecialAbilities[id],
getStaticVisionById: id => database.visions[id],
getStaticMagicalTraditionById: id => database.magicalTraditions[id],
getStaticBlessedTraditionById: id => database.blessedTraditions[id],
getStaticPactGiftById: id => database.pactGifts[id],
getStaticVampiricGiftById: id => database.vampiricGifts[id],
getStaticSikaryanDrainSpecialAbilityById: id => database.sikaryanDrainSpecialAbilities[id],
getStaticLycantropicGiftById: id => database.lycantropicGifts[id],
getStaticSkillStyleSpecialAbilityById: id => database.skillStyleSpecialAbilities[id],
getStaticAdvancedSkillSpecialAbilityById: id => database.advancedSkillSpecialAbilities[id],
getStaticArcaneOrbEnchantmentById: id => database.arcaneOrbEnchantments[id],
getStaticCauldronEnchantmentById: id => database.cauldronEnchantments[id],
getStaticFoolsHatEnchantmentById: id => database.foolsHatEnchantments[id],
getStaticToyEnchantmentById: id => database.toyEnchantments[id],
getStaticBowlEnchantmentById: id => database.bowlEnchantments[id],
getStaticFatePointSexSpecialAbilityById: id => database.fatePointSexSpecialAbilities[id],
getStaticSexSpecialAbilityById: id => database.sexSpecialAbilities[id],
getStaticWeaponEnchantmentById: id => database.weaponEnchantments[id],
getStaticSickleRitualById: id => database.sickleRituals[id],
getStaticRingEnchantmentById: id => database.ringEnchantments[id],
getStaticChronicleEnchantmentById: id => database.chronicleEnchantments[id],
getStaticKrallenkettenzauberById: id => database.krallenkettenzauber[id],
getStaticTrinkhornzauberById: id => database.trinkhornzauber[id],
getStaticMagicalSignById: id => database.magicalSigns[id],
getStaticSpellById: id => database.spells[id],
getStaticRitualById: id => database.rituals[id],
getStaticLiturgicalChantById: id => database.liturgicalChants[id],
getStaticCeremonyById: id => database.ceremonies[id],
...activatablePrerequisiteCapabilities,
...ancestorBloodPrerequisiteCapabilities,
...blessedTraditionPrerequisiteCapabilities,
...commonSuggestedByRCPPrerequisiteCapabilities,
...culturePrerequisiteCapabilities,
...externalEnhancementPrerequisiteCapabilities,
...influencePrerequisiteCapabilities,
...magicalTraditionPrerequisiteCapabilities,
...pactPrerequisiteCapabilities,
...primaryAttributePrerequisiteCapabilities,
...publicationPrerequisiteCapabilities,
...racePrerequisiteCapabilities,
...ratedMinimumNumberPrerequisiteCapabilities,
...ratedPrerequisiteCapabilities,
...ratedSumPrerequisiteCapabilities,
...rulePrerequisiteCapabilities,
...sexPrerequisiteCapabilities,
...sexualCharacteristicPrerequisiteCapabilities,
...socialStatusPrerequisiteCapabilities,
...statePrerequisiteCapabilities,
...textPrerequisiteCapabilities,
}),
pre => checkPreconditions(pre, preconditionCapabilities),
),
)
@@ -61,8 +61,8 @@ export const selectAvailableHairColorsIdDice = createSelector(
selectCurrentRaceVariant,
createPropertySelector(selectDynamicDisadvantages, DisadvantageIdentifier.Stigma),
(currentRace, currentRaceVariant, stigma): number[] => {
const isAlbino = isOptionActive(stigma, { type: "Generic", value: ALBINO })
const isGreenHaired = isOptionActive(stigma, { type: "Generic", value: GREEN_HAIR })
const isAlbino = isOptionActive(stigma, { tag: "General", general: ALBINO })
const isGreenHaired = isOptionActive(stigma, { tag: "General", general: GREEN_HAIR })
if (isAlbino || isGreenHaired) {
return filterNonNullable([
@@ -103,7 +103,7 @@ export const selectAvailableEyeColorsIdDice = createSelector(
selectCurrentRaceVariant,
createPropertySelector(selectDynamicDisadvantages, DisadvantageIdentifier.Stigma),
(currentRace, currentRaceVariant, stigma): number[] => {
const isAlbino = isOptionActive(stigma, { type: "Generic", value: ALBINO })
const isAlbino = isOptionActive(stigma, { tag: "General", general: ALBINO })
if (isAlbino) {
const eyeColorIds = [EyeColorIdentifier.Red, EyeColorIdentifier.Purple]
@@ -127,6 +127,7 @@ export const selectCapabilitiesForActivatablePrerequisite = createSelector(
getDynamicMagicalTraditionById: id => specialAbilities?.magicalTraditions?.[id],
getDynamicBlessedTraditionById: id => specialAbilities?.blessedTraditions?.[id],
getDynamicPactGiftById: id => specialAbilities?.pactGifts?.[id],
getDynamicVampiricGiftById: id => specialAbilities?.vampiricGifts?.[id],
getDynamicSikaryanDrainSpecialAbilityById: id =>
specialAbilities?.sikaryanDrainSpecialAbilities?.[id],
getDynamicLycantropicGiftById: id => specialAbilities?.lycantropicGifts?.[id],
@@ -148,6 +149,7 @@ export const selectCapabilitiesForActivatablePrerequisite = createSelector(
getDynamicChronicleEnchantmentById: id => specialAbilities?.chronicleEnchantments?.[id],
getDynamicKrallenkettenzauberById: id => specialAbilities?.krallenkettenzauber?.[id],
getDynamicTrinkhornzauberById: id => specialAbilities?.trinkhornzauber?.[id],
getDynamicMagicalSignById: id => specialAbilities?.magicalSigns?.[id],
checkPrecondition: pre => checkPrecondition(precondition, pre),
}),
)
@@ -386,22 +388,8 @@ export const selectCapabilitiesForRatedPrerequisite = createSelector(
*/
export const selectCapabilitiesForRatedSumPrerequisite = createSelector(
SelectGetById.Dynamic.Skill,
SelectGetById.Dynamic.Spell,
SelectGetById.Dynamic.Ritual,
SelectGetById.Dynamic.LiturgicalChant,
SelectGetById.Dynamic.Ceremony,
(
(getDynamicSkillById): Parameters<typeof checkRatedSumPrerequisite>[0] => ({
getDynamicSkillById,
getDynamicSpellById,
getDynamicRitualById,
getDynamicLiturgicalChantById,
getDynamicCeremonyById,
): Parameters<typeof checkRatedSumPrerequisite>[0] => ({
getDynamicSkillById,
getDynamicSpellById,
getDynamicRitualById,
getDynamicLiturgicalChantById,
getDynamicCeremonyById,
}),
)
@@ -1,12 +1,12 @@
import { createSelector } from "@reduxjs/toolkit"
import { isActive } from "../../shared/domain/activatable/activatableEntry.ts"
import { getMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages } from "../../shared/domain/activatable/advantagesDisadvantages.ts"
import {
CombinedActiveBlessedTradition,
isBlessedOne,
} from "../../shared/domain/activatable/blessedTradition.ts"
import {
CombinedActiveMagicalTradition,
getMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages,
isSpellcaster,
} from "../../shared/domain/activatable/magicalTradition.ts"
import { AdvantageIdentifier } from "../../shared/domain/identifier.ts"
+293
View File
@@ -0,0 +1,293 @@
import { ActionCreatorWithPayload, AnyAction, Draft, createAction } from "@reduxjs/toolkit"
import { AdventurePointsValue } from "optolith-database-schema/types/_Activatable"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { getCost } from "../../shared/domain/activatable/activatableActive.ts"
import {
Activatable,
ActivatableInstance,
ActivatableMap,
createEmptyDynamicActivatable,
} from "../../shared/domain/activatable/activatableEntry.ts"
import { getSelectOptionsFromCacheById } from "../../shared/domain/database.ts"
import { RegistrationMethod } from "../../shared/domain/dependencies/registrationHelpers.ts"
import { createIdentifierObject, equalsIdentifier } from "../../shared/domain/identifier.ts"
import { hasAspectById } from "../../shared/domain/rated/liturgicalChant.ts"
import { RangeBounds } from "../../shared/utils/range.ts"
import { DraftReducer } from "../../shared/utils/redux.ts"
import { CharacterState } from "./characterSlice.ts"
import { DatabaseState } from "./databaseSlice.ts"
/**
* Functions for working with rated entries that can be activated and have
* enhancements.
*/
export type ActivatableSlice<N extends string, E extends string> = {
/**
* The actions that can be dispatched to modify the state.
*/
actions: {
/**
* Add the entry with the given id.
*/
addInstance: ActionCreatorWithPayload<
{ id: number; instance: ActivatableInstance },
`${N}/add${E}`
>
/**
* Change the level of the entry with the given id.
*/
changeInstanceLevel: ActionCreatorWithPayload<
{ id: number; index: number; level: number },
`${N}/change${E}Level`
>
/**
* Remove the entry with the given id.
*/
removeInstance: ActionCreatorWithPayload<{ id: number; index: number }, `${N}/remove${E}`>
}
/**
* The reducer that handles the actions.
*/
reducer: DraftReducer<CharacterState, AnyAction, [database: DatabaseState]>
}
const tagToNamespace = {
AdvancedCombatSpecialAbility: "advancedCombatSpecialAbilities",
AdvancedKarmaSpecialAbility: "advancedKarmaSpecialAbilities",
AdvancedMagicalSpecialAbility: "advancedMagicalSpecialAbilities",
AdvancedSkillSpecialAbility: "advancedSkillSpecialAbilities",
Advantage: "advantages",
AncestorGlyph: "ancestorGlyphs",
ArcaneOrbEnchantment: "arcaneOrbEnchantments",
AttireEnchantment: "attireEnchantments",
BlessedTradition: "blessedTraditions",
BowlEnchantment: "bowlEnchantments",
BrawlingSpecialAbility: "brawlingSpecialAbilities",
CauldronEnchantment: "cauldronEnchantments",
CeremonialItemSpecialAbility: "ceremonialItemSpecialAbilities",
ChronicleEnchantment: "chronicleEnchantments",
CombatSpecialAbility: "combatSpecialAbilities",
CombatStyleSpecialAbility: "combatStyleSpecialAbilities",
CommandSpecialAbility: "commandSpecialAbilities",
DaggerRitual: "daggerRituals",
Disadvantage: "disadvantages",
FamiliarSpecialAbility: "familiarSpecialAbilities",
FatePointSexSpecialAbility: "fatePointSexSpecialAbilities",
FatePointSpecialAbility: "fatePointSpecialAbilities",
FoolsHatEnchantment: "foolsHatEnchantments",
GeneralSpecialAbility: "generalSpecialAbilities",
InstrumentEnchantment: "instrumentEnchantments",
KarmaSpecialAbility: "karmaSpecialAbilities",
Krallenkettenzauber: "krallenkettenzauber",
LiturgicalStyleSpecialAbility: "liturgicalStyleSpecialAbilities",
LycantropicGift: "lycantropicGifts",
MagicalRune: "magicalRunes",
MagicalSign: "magicalSigns",
MagicalSpecialAbility: "magicalSpecialAbilities",
MagicalTradition: "magicalTraditions",
MagicStyleSpecialAbility: "magicStyleSpecialAbilities",
OrbEnchantment: "orbEnchantments",
PactGift: "pactGifts",
ProtectiveWardingCircleSpecialAbility: "protectiveWardingCircleSpecialAbilities",
RingEnchantment: "ringEnchantments",
Sermon: "sermons",
SexSpecialAbility: "sexSpecialAbilities",
SickleRitual: "sickleRituals",
SikaryanDrainSpecialAbility: "sikaryanDrainSpecialAbilities",
SkillStyleSpecialAbility: "skillStyleSpecialAbilities",
SpellSwordEnchantment: "spellSwordEnchantments",
StaffEnchantment: "staffEnchantments",
ToyEnchantment: "toyEnchantments",
Trinkhornzauber: "trinkhornzauber",
VampiricGift: "vampiricGifts",
Vision: "visions",
WandEnchantment: "wandEnchantments",
WeaponEnchantment: "weaponEnchantments",
} as const
type RecordValue<R> = R extends Record<number, infer V> ? V : never
/**
* Creates a slice for a map of rated entries.
*/
export const createActivatableSlice = <
E extends ActivatableIdentifier["tag"],
P,
IdO extends { tag: string },
N extends (typeof tagToNamespace)[E] = (typeof tagToNamespace)[E],
>(config: {
entityName: E
getState: (state: Draft<CharacterState>) => Draft<ActivatableMap>
getPrerequisites: (id: number, database: DatabaseState) => P[]
getAdventurePointsValue: (staticEntry: RecordValue<DatabaseState[N]>) => AdventurePointsValue
createIdentifierObject: (id: number) => IdO
registerOrUnregisterPrerequisitesAsDependencies: (
method: RegistrationMethod,
character: Draft<CharacterState>,
prerequisites: P[],
sourceId: IdO,
levelRange: RangeBounds,
capabilities: {
ancestorBloodAdvantageIds: number[]
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
},
) => void
}): ActivatableSlice<N, E> => {
const namespace = tagToNamespace[config.entityName] as N
const updateCachedAdventurePoints = (entry: Draft<Activatable>, database: DatabaseState) => {
const staticEntry = database[namespace][entry.id]
const apValue =
staticEntry === undefined
? undefined
: config.getAdventurePointsValue(staticEntry as RecordValue<DatabaseState[N]>)
if (apValue !== undefined) {
const id = createIdentifierObject(config.entityName, entry.id)
entry.cachedAdventurePoints = {
general: getCost(id, apValue, entry.instances, optionId =>
getSelectOptionsFromCacheById(database.cache.activatableSelectOptions, id)?.find(opt =>
equalsIdentifier(opt.id, optionId),
),
).total,
bound: 0,
}
}
return entry
}
const addAction = createAction<{ id: number; instance: ActivatableInstance }, `${N}/add${E}`>(
`${namespace}/add${config.entityName}`,
)
const changeLevelAction = createAction<
{ id: number; index: number; level: number },
`${N}/change${E}Level`
>(`${namespace}/change${config.entityName}Level`)
const removeAction = createAction<{ id: number; index: number }, `${N}/remove${E}`>(
`${namespace}/remove${config.entityName}`,
)
const getCapabilities = (
database: DatabaseState,
): Parameters<(typeof config)["registerOrUnregisterPrerequisitesAsDependencies"]>[5] => ({
ancestorBloodAdvantageIds: database.cache.ancestorBloodAdvantages.ids,
closeCombatTechniqueIds: Object.values(database.closeCombatTechniques).map(x => x.id),
rangedCombatTechniqueIds: Object.values(database.rangedCombatTechniques).map(x => x.id),
blessedTraditionChurchIds: Object.values(database.blessedTraditions)
.filter(x => !x.is_shamanistic)
.map(x => x.id),
blessedTraditionShamanisticIds: Object.values(database.blessedTraditions)
.filter(x => x.is_shamanistic)
.map(x => x.id),
magicalTraditionIds: Object.values(database.magicalTraditions).map(x => x.id),
magicalTraditionIdsThatCanLearnRituals: Object.values(database.magicalTraditions)
.filter(x => x.can_learn_rituals)
.map(x => x.id),
magicalTraditionIdsThatCanBindFamiliars: Object.values(database.magicalTraditions)
.filter(x => x.can_bind_familiars)
.map(x => x.id),
getSpellIdsByPropertyId: id =>
Object.values(database.spells)
.filter(x => x.property.id.property === id)
.map(x => x.id),
getRitualIdsByPropertyId: id =>
Object.values(database.rituals)
.filter(x => x.property.id.property === id)
.map(x => x.id),
getLiturgicalChantIdsByAspectId: id =>
Object.values(database.liturgicalChants)
.filter(x => hasAspectById(id, x.traditions))
.map(x => x.id),
getCeremonyIdsByAspectId: id =>
Object.values(database.ceremonies)
.filter(x => hasAspectById(id, x.traditions))
.map(x => x.id),
})
const reducer: DraftReducer<CharacterState, AnyAction, [database: DatabaseState]> = (
state,
action,
database,
) => {
if (addAction.match(action)) {
const { id, instance } = action.payload
const entry = (config.getState(state)[id] ??= createEmptyDynamicActivatable(id))
entry.instances.push(instance)
updateCachedAdventurePoints(entry, database)
config.registerOrUnregisterPrerequisitesAsDependencies(
RegistrationMethod.Remove,
state,
config.getPrerequisites(action.payload.id, database),
config.createIdentifierObject(action.payload.id),
[1, instance.level ?? 1],
getCapabilities(database),
)
} else if (changeLevelAction.match(action)) {
const { id, index, level } = action.payload
const entry = config.getState(state)[id]
const instance = entry?.instances[index]
if (instance !== undefined && instance.level !== undefined && instance.level !== level) {
instance.level = level
updateCachedAdventurePoints(entry!, database)
if (level < instance.level) {
config.registerOrUnregisterPrerequisitesAsDependencies(
RegistrationMethod.Remove,
state,
config.getPrerequisites(action.payload.id, database),
config.createIdentifierObject(action.payload.id),
[level + 1, instance.level],
getCapabilities(database),
)
} else if (level > instance.level) {
config.registerOrUnregisterPrerequisitesAsDependencies(
RegistrationMethod.Add,
state,
config.getPrerequisites(action.payload.id, database),
config.createIdentifierObject(action.payload.id),
[instance.level + 1, level],
getCapabilities(database),
)
}
}
} else if (removeAction.match(action)) {
const { id, index } = action.payload
const entry = config.getState(state)[id]
if (entry !== undefined) {
config.registerOrUnregisterPrerequisitesAsDependencies(
RegistrationMethod.Remove,
state,
config.getPrerequisites(action.payload.id, database),
config.createIdentifierObject(action.payload.id),
[1, entry.instances[index]!.level ?? 1],
getCapabilities(database),
)
entry.instances.splice(index, 1)
updateCachedAdventurePoints(entry, database)
}
}
}
return {
actions: {
addInstance: addAction,
changeInstanceLevel: changeLevelAction,
removeInstance: removeAction,
},
reducer,
}
}
+21
View File
@@ -0,0 +1,21 @@
import { registerOrUnregisterPrerequisitesOfAdvantageOrDisadvantageAsDependencies } from "../../shared/domain/dependencies/fullPrerequisiteRegistrationAsDependencyForType.ts"
import { createIdentifierObject } from "../../shared/domain/identifier.ts"
import { createActivatableSlice } from "./activatableSlice.ts"
// eslint-disable-next-line jsdoc/require-jsdoc
export const {
actions: {
addInstance: addAdvantage,
changeInstanceLevel: changeAdvantageLevel,
removeInstance: removeAdvantage,
},
reducer: advantagesReducer,
} = createActivatableSlice({
entityName: "Advantage",
getState: state => state.advantages,
getPrerequisites: (id, database) => database.advantages[id]?.prerequisites ?? [],
getAdventurePointsValue: staticEntry => staticEntry.ap_value,
createIdentifierObject: id => createIdentifierObject("Advantage", id),
registerOrUnregisterPrerequisitesAsDependencies:
registerOrUnregisterPrerequisitesOfAdvantageOrDisadvantageAsDependencies,
})
+32
View File
@@ -0,0 +1,32 @@
/* eslint-disable jsdoc/require-jsdoc */
/* eslint-disable max-len */
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { RootState } from "../store.ts"
export type Alert = {
title: string
description: string
}
type AlertsState = Alert[]
const initialAlertsState: AlertsState = []
const alertsSlice = createSlice({
name: "alerts",
initialState: initialAlertsState,
reducers: {
showAlert: (state, action: PayloadAction<Alert>) => {
state.push(action.payload)
},
dismissAlert: state => {
state.splice(0, 1)
},
},
})
export const { showAlert, dismissAlert } = alertsSlice.actions
export const selectCurrentAlert = (state: RootState) => state.alerts[0]
export const alertsReducer = alertsSlice.reducer
+22 -19
View File
@@ -8,6 +8,7 @@ import {
} from "../../shared/domain/identifier.ts"
import { createImmerReducer, reduceReducers } from "../../shared/utils/redux.ts"
import { RootState } from "../store.ts"
import { advantagesReducer } from "./advantagesSlice.ts"
import { attributesReducer } from "./attributesSlice.ts"
import { blessingsReducer } from "./blessingsSlice.ts"
import { cantripsReducer } from "./cantripsSlice.ts"
@@ -15,6 +16,7 @@ import { ceremoniesReducer } from "./ceremoniesSlice.ts"
import { closeCombatTechniquesReducer } from "./closeCombatTechniqueSlice.ts"
import { DatabaseState } from "./databaseSlice.ts"
import { derivedCharacteristicsReducer } from "./derivedCharacteristicsSlice.ts"
import { disadvantagesReducer } from "./disadvantagesSlice.ts"
import { liturgicalChantsReducer } from "./liturgicalChantsSlice.ts"
import { animistPowersReducer } from "./magicalActions/animistPowersSlice.ts"
import { cursesReducer } from "./magicalActions/cursesSlice.ts"
@@ -74,14 +76,12 @@ const staticInitialState: Omit<CharacterState, "dateCreated" | "dateLastModified
},
},
advantages: {
[AdvantageIdentifier.Blessed]: {
id: AdvantageIdentifier.Blessed,
instances: [{}],
},
[AdvantageIdentifier.Spellcaster]: {
id: AdvantageIdentifier.Spellcaster,
instances: [{}],
},
[AdvantageIdentifier.Blessed]: createDynamicActivatable(AdvantageIdentifier.Blessed, [{}], 25),
[AdvantageIdentifier.Spellcaster]: createDynamicActivatable(
AdvantageIdentifier.Spellcaster,
[{}],
25,
),
},
disadvantages: {},
specialAbilities: {
@@ -93,10 +93,11 @@ const staticInitialState: Omit<CharacterState, "dateCreated" | "dateLastModified
arcaneOrbEnchantments: {},
attireEnchantments: {},
blessedTraditions: {
[BlessedTraditionIdentifier.Praios]: {
id: BlessedTraditionIdentifier.Praios,
instances: [{}],
},
[BlessedTraditionIdentifier.Praios]: createDynamicActivatable(
BlessedTraditionIdentifier.Praios,
[{}],
130,
),
},
bowlEnchantments: {},
brawlingSpecialAbilities: {},
@@ -121,10 +122,11 @@ const staticInitialState: Omit<CharacterState, "dateCreated" | "dateLastModified
magicalSigns: {},
magicalSpecialAbilities: {},
magicalTraditions: {
[MagicalTraditionIdentifier.Witches]: {
id: MagicalTraditionIdentifier.Witches,
instances: [{}],
},
[MagicalTraditionIdentifier.Witches]: createDynamicActivatable(
MagicalTraditionIdentifier.Witches,
[{}],
135,
),
},
magicStyleSpecialAbilities: {},
orbEnchantments: {},
@@ -145,7 +147,8 @@ const staticInitialState: Omit<CharacterState, "dateCreated" | "dateLastModified
wandEnchantments: {},
weaponEnchantments: {},
},
attributes: {},
magicalPrimaryAttributeDependencies: [],
blessedPrimaryAttributeDependencies: [],
derivedCharacteristics: {
lifePoints: {
purchased: 0,
@@ -242,8 +245,6 @@ const staticInitialState: Omit<CharacterState, "dateCreated" | "dateLastModified
value: 0,
},
},
magicalPrimaryAttributeDependencies: [],
blessedPrimaryAttributeDependencies: [],
// items: {}
// hitZoneArmors: {}
purse: {
@@ -1012,6 +1013,8 @@ export const characterReducer = reduceReducers<
raceReducer,
professionReducer,
rulesReducer,
advantagesReducer,
disadvantagesReducer,
skillsReducer,
closeCombatTechniquesReducer,
rangedCombatTechniquesReducer,
@@ -0,0 +1,21 @@
import { registerOrUnregisterPrerequisitesOfAdvantageOrDisadvantageAsDependencies } from "../../shared/domain/dependencies/fullPrerequisiteRegistrationAsDependencyForType.ts"
import { createIdentifierObject } from "../../shared/domain/identifier.ts"
import { createActivatableSlice } from "./activatableSlice.ts"
// eslint-disable-next-line jsdoc/require-jsdoc
export const {
actions: {
addInstance: addDisadvantage,
changeInstanceLevel: changeDisadvantageLevel,
removeInstance: removeDisadvantage,
},
reducer: disadvantagesReducer,
} = createActivatableSlice({
entityName: "Disadvantage",
getState: state => state.disadvantages,
getPrerequisites: (id, database) => database.disadvantages[id]?.prerequisites ?? [],
getAdventurePointsValue: staticEntry => staticEntry.ap_value,
createIdentifierObject: id => createIdentifierObject("Disadvantage", id),
registerOrUnregisterPrerequisitesAsDependencies:
registerOrUnregisterPrerequisitesOfAdvantageOrDisadvantageAsDependencies,
})
@@ -50,8 +50,10 @@ export type DisplayableMainIdentifier =
| ID.EquipmentOfBlessedOnesIdentifier
| ID.ExperienceLevelIdentifier
| ID.FamiliarSpecialAbilityIdentifier
| ID.FatePointSpecialAbilityIdentifier
| ID.FatePointSexSpecialAbilityIdentifier
| ID.FocusRuleIdentifier
| ID.FoolsHatEnchantmentIdentifier
| ID.GemOrPreciousStoneIdentifier
| ID.GeneralSpecialAbilityIdentifier
| ID.GeodeRitualIdentifier
@@ -72,6 +74,8 @@ export type DisplayableMainIdentifier =
| ID.MagicalArtifactIdentifier
| ID.MagicalDanceIdentifier
| ID.MagicalMelodyIdentifier
| ID.MagicalRuneIdentifier
| ID.MagicalSignIdentifier
| ID.MagicalSpecialAbilityIdentifier
| ID.MagicalTraditionIdentifier
| ID.MusicalInstrumentIdentifier
@@ -93,12 +97,16 @@ export type DisplayableMainIdentifier =
| ID.SickleRitualIdentifier
| ID.SikaryanDrainSpecialAbilityIdentifier
| ID.SkillIdentifier
| ID.SkillStyleSpecialAbilityIdentifier
| ID.SpellIdentifier
| ID.SpellSwordEnchantmentIdentifier
| ID.StaffEnchantmentIdentifier
| ID.ThievesToolIdentifier
| ID.ToolOfTheTradeIdentifier
| ID.ToyEnchantmentIdentifier
| ID.TravelGearOrToolIdentifier
| ID.TrinkhornzauberIdentifier
| ID.VampiricGiftIdentifier
| ID.VehicleIdentifier
| ID.VisionIdentifier
| ID.WandEnchantmentIdentifier
+2
View File
@@ -1,5 +1,6 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit"
import { reduceReducers } from "../shared/utils/redux.ts"
import { alertsReducer } from "./slices/alertsSlice.ts"
import { charactersReducer } from "./slices/charactersSlice.ts"
import { databaseReducer } from "./slices/databaseSlice.ts"
import { globalReducer } from "./slices/global.ts"
@@ -8,6 +9,7 @@ import { routeReducer } from "./slices/routeSlice.ts"
import { settingsReducer } from "./slices/settingsSlice.ts"
const sliceReducer = combineReducers({
alerts: alertsReducer,
database: databaseReducer,
characters: charactersReducer,
inlineLibrary: inlineLibraryReducer,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,85 @@
import {
ActivatableIdentifier,
RequirableSelectOptionIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import { Preconditions } from "optolith-database-schema/types/prerequisites/ConditionalPrerequisites"
import { equalsIdentifier } from "../identifier.ts"
import { ActivatableInstance, PredefinedActivatableOption } from "./activatableEntry.ts"
/**
* A dependency on an activatable entry.
*/
export type ActivatableDependency = 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
/**
* If the required entry should be required to be active or inactive.
*/
active: boolean
/**
* The required minimum level of the entry.
*/
level?: number
/**
* Required select options. Order is important. Typically, you only need the
* first array index, though.
*/
options?: RequirableSelectOptionIdentifier[]
when?: Preconditions
}>
/**
* Returns whether the given dependency is a general dependency, i.e. it does
* not require a specific level or options, just the entry in general.
*/
export const isGeneralDependency = (dependency: ActivatableDependency) =>
dependency.level === undefined && dependency.options === undefined
/**
* Returns whether the given dependency strictly, i.e. with exactly matching
* options array, applies to the given activatable instance.
*/
export const appliesToInstanceStrict = (
dependency: ActivatableDependency,
instance: ActivatableInstance,
) =>
dependency.options === undefined ||
dependency.options.every(
(depOpt, i) =>
instance.options?.[i]?.type === "Predefined" &&
equalsIdentifier(depOpt, (instance.options![i] as PredefinedActivatableOption).id),
)
/**
* Returns whether the given dependency applies loosely, i.e. the options from
* the options array are included in the instance options, to the given
* activatable instance.
*/
export const appliesToInstanceLoose = (
dependency: ActivatableDependency,
instance: ActivatableInstance,
) =>
dependency.options === undefined ||
dependency.options.every(
depOpt =>
instance.options?.some(
opt => opt.type === "Predefined" && equalsIdentifier(depOpt, opt.id),
) ?? false,
)
+115 -89
View File
@@ -1,7 +1,9 @@
import { RequirableSelectOptionIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { Equality } from "../../utils/compare.ts"
import { SelectOptionIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { isNotNullish } from "../../utils/nullable.ts"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { BoundAdventurePointsForActivatable } from "../adventurePoints/activatableEntry.ts"
import { AdventurePointsCache, emptyAdventurePointsCache } from "../adventurePoints/cache.ts"
import { equalsIdentifier, splitIdentifierObject } from "../identifier.ts"
import { ActivatableDependency } from "./activatableDependency.ts"
/**
* An activated activatable identifier.
@@ -27,7 +29,7 @@ export const isTinyActivatableActive = (activatable: TinyActivatable | undefined
/**
* An activatable entry.
*/
export type Activatable = {
export type Activatable = Readonly<{
/**
* The activatable identifier.
* @integer
@@ -38,7 +40,25 @@ export type Activatable = {
* One or multiple activations of the activatable.
*/
instances: ActivatableInstance[]
}
/**
* The accumulated used adventure points value of all instances.
*/
cachedAdventurePoints: AdventurePointsCache
/**
* The activatable's dependencies.
*/
dependencies: ActivatableDependency[]
/**
* A list of bound adventure points. Bound adventure points are granted by the
* GM and can only be spent on the entry. They dont affect the costs of
* buying the activatable entry. They may specify for which configuration of
* an entry they count, just like a prerequisite.
*/
boundAdventurePoints: BoundAdventurePointsForActivatable[]
}>
/**
* A single activation of an activatable entry.
@@ -59,6 +79,11 @@ export type ActivatableInstance = {
* If provided, a custom adventure points value has been set for this instance.
*/
customAdventurePointsValue?: number
/**
* A list of used bound adventure points for this instance.
*/
usedBoundAdventurePoints?: BoundAdventurePointsForActivatable[]
}
/**
@@ -75,44 +100,16 @@ export type PredefinedActivatableOption = {
/**
* An identifier referencing a different entry.
*/
id: {
/**
* The entry type or `"Generic"` if it references a select option local to the entry.
*/
type:
| "Generic"
| "Blessing"
| "Cantrip"
| "TradeSecret"
| "Script"
| "AnimalShape"
| "ArcaneBardTradition"
| "ArcaneDancerTradition"
| "SexPractice"
| "Race"
| "Culture"
| "BlessedTradition"
| "Element"
| "Property"
| "Aspect"
| "Disease"
| "Poison"
| "Language"
| "Skill"
| "CloseCombatTechnique"
| "RangedCombatTechnique"
| "LiturgicalChant"
| "Ceremony"
| "Spell"
| "Ritual"
/**
* The numeric identifier.
*/
value: number
}
id: SelectOptionIdentifier
}
/**
* A predicate for a predefined option on an activatable entry.
*/
export const isPredefinedActivatableOption = (
option: ActivatableOption,
): option is PredefinedActivatableOption => option.type === "Predefined"
/**
* A custom option for an activatable entry, handled as user-entered text.
*/
@@ -125,6 +122,19 @@ export type CustomActivatableOption = {
value: string
}
/**
* A predicate for a custom option on an activatable entry.
*/
export const isCustomActivatableOption = (
option: ActivatableOption,
): option is CustomActivatableOption => option.type === "Custom"
/**
* Returns whether a select option matches a given activatable option.
*/
export const matchesOptionId = (id: SelectOptionIdentifier, option: ActivatableOption) =>
isPredefinedActivatableOption(option) && equalsIdentifier(option.id, id)
/**
* A map of activatable entries.
*/
@@ -138,10 +148,32 @@ export type ActivatableMap = {
export const createEmptyDynamicActivatable = (id: number): Activatable => ({
id,
instances: [],
cachedAdventurePoints: emptyAdventurePointsCache,
dependencies: [],
boundAdventurePoints: [],
})
const equalOptionId: Equality<PredefinedActivatableOption["id"]> = (a, b) =>
a.type === b.type && a.value === b.value
/**
* Creates a dynamic activatable entry.
*/
export const createDynamicActivatable = (
id: number,
instances: ActivatableInstance[],
cachedAdventurePoints: AdventurePointsCache | number,
options: {
dependencies?: ActivatableDependency[]
boundAdventurePoints?: BoundAdventurePointsForActivatable[]
} = {},
): Activatable => ({
id,
instances,
cachedAdventurePoints:
typeof cachedAdventurePoints === "number"
? { general: cachedAdventurePoints, bound: 0 }
: cachedAdventurePoints,
dependencies: options.dependencies ?? [],
boundAdventurePoints: options.boundAdventurePoints ?? [],
})
/**
* Get the level of the first instance of a given activatable entry, if it is
@@ -156,6 +188,12 @@ export const firstLevel = (activatable: Activatable | undefined) =>
export const isActive = (activatable: Activatable | undefined): activatable is Activatable =>
(activatable?.instances.length ?? 0) > 0
/**
* Return whether the instance has a custom adventure points value.
*/
export const hasCustomAdventurePointsValue = (instance: ActivatableInstance): boolean =>
instance.customAdventurePointsValue !== undefined
/**
* Returns if a given option is active in any instance of a given activatable.
* It defaults to the first option in the options array, but a different index
@@ -168,23 +206,37 @@ export const isOptionActive = (
): boolean =>
activatable?.instances.some(instance => {
const optionAtIndex = instance.options?.[atIndex]
return optionAtIndex?.type === "Predefined" && equalOptionId(optionAtIndex.id, optionId)
return optionAtIndex?.type === "Predefined" && equalsIdentifier(optionAtIndex.id, optionId)
}) ?? false
/**
* Returns the number of instances of a given activatable entry.
*/
export const countActivations = (activatable: Activatable | undefined): number =>
activatable?.instances.length ?? 0
/**
* Returns the number of instances of a given activatable entry that have a
* given option active. It defaults to the first option in the options array,
* but a different index can be specified.
*/
export const countOptions = (
activatable: Activatable | undefined,
optionId: PredefinedActivatableOption["id"],
atIndex = 0,
): number =>
activatable?.instances.filter(instance => {
const optionAtIndex = instance.options?.[atIndex]
return optionAtIndex?.type === "Predefined" && equalOptionId(optionAtIndex.id, optionId)
}).length ?? 0
activatable: Activatable | undefined | ActivatableInstance[],
optionId: SelectOptionIdentifier,
options: { atIndex?: number; ignoreWithCustomAdventurePointsValue?: boolean } = {},
): number => {
const { atIndex = 0, ignoreWithCustomAdventurePointsValue = false } = options
return (Array.isArray(activatable) ? activatable : activatable?.instances ?? []).filter(
instance => {
const optionAtIndex = instance.options?.[atIndex]
return (
optionAtIndex?.type === "Predefined" &&
equalsIdentifier(optionAtIndex.id, optionId) &&
(!ignoreWithCustomAdventurePointsValue || !hasCustomAdventurePointsValue(instance))
)
},
).length
}
/**
* Returns the option value of the first instance of a given activatable entry.
@@ -192,9 +244,10 @@ export const countOptions = (
* can be specified.
*/
export const getFirstOption = (
activatable: Activatable | undefined,
activatable: Activatable | undefined | ActivatableInstance[],
atIndex = 0,
): ActivatableOption | undefined => activatable?.instances[0]?.options?.[atIndex]
): ActivatableOption | undefined =>
(Array.isArray(activatable) ? activatable : activatable?.instances ?? [])[0]?.options?.[atIndex]
/**
* Returns the option value of the first instance of a given activatable entry
@@ -202,13 +255,13 @@ export const getFirstOption = (
* in the options array, but a different index can be specified.
*/
export const getFirstOptionOfType = (
activatable: Activatable | undefined,
type: PredefinedActivatableOption["id"]["type"],
activatable: Activatable | undefined | ActivatableInstance[],
tag: SelectOptionIdentifier["tag"],
atIndex = 0,
): number | undefined => {
const firstOption = getFirstOption(activatable, atIndex)
if (firstOption?.type === "Predefined" && firstOption.id.type === type) {
return firstOption.id.value
if (firstOption?.type === "Predefined" && firstOption.id.tag === tag) {
return splitIdentifierObject(firstOption.id)[1]
}
return undefined
}
@@ -219,36 +272,9 @@ export const getFirstOptionOfType = (
* specified.
*/
export const getOptions = (
activatable: Activatable | undefined,
activatable: Activatable | undefined | ActivatableInstance[],
atIndex = 0,
): ActivatableOption[] =>
activatable?.instances.map(instance => instance.options?.[atIndex]).filter(isNotNullish) ?? []
/**
* Checks if a prerequisite option and an instance option are equal.
*/
export const equalsOptionPrerequisite = (
instanceOption: PredefinedActivatableOption["id"],
prerequisiteOption: RequirableSelectOptionIdentifier,
): boolean => {
switch (prerequisiteOption.tag) {
case "General":
return (
instanceOption.type === "Generic" && prerequisiteOption.general === instanceOption.value
)
case "Skill":
return instanceOption.type === "Skill" && prerequisiteOption.skill === instanceOption.value
case "CloseCombatTechnique":
return (
instanceOption.type === "CloseCombatTechnique" &&
prerequisiteOption.close_combat_technique === instanceOption.value
)
case "RangedCombatTechnique":
return (
instanceOption.type === "RangedCombatTechnique" &&
prerequisiteOption.ranged_combat_technique === instanceOption.value
)
default:
return assertExhaustive(prerequisiteOption)
}
}
(Array.isArray(activatable) ? activatable : activatable?.instances ?? [])
.map(instance => instance.options?.[atIndex])
.filter(isNotNullish)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
import { ResolvedSelectOption } from "optolith-database-schema/cache/activatableSelectOptions"
import { Advantage } from "optolith-database-schema/types/Advantage"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { FilterApplyingActivatableDependencies } from "../dependencies/filterApplyingDependencies.ts"
import { All, GetById, Singleton } from "../getTypes.ts"
import { getCreateIdentifierObject } from "../identifier.ts"
import { checkPrerequisitesOfAdvantageOrDisadvantage } from "../prerequisites/fullPrerequisiteValidationForType.ts"
import { DisplayedActiveActivatable, getActiveActivatables } from "./activatableActive.ts"
/**
* A combination of a static advantage and its dynamic counterpart, extended by
* which activation/instance of the entry it represents.
*/
export type DisplayedActiveAdvantage = DisplayedActiveActivatable<Advantage>
/**
* Returns all advantages with their corresponding dynamic entry, extended by
* which activation/instance of the entry it represents.
*/
export const getActiveAdvantages = (
getStaticAdvantageById: GetById.Static.Advantage,
dynamicAdvantages: All.Dynamic.Advantages,
prerequisiteCapabilities: Parameters<typeof checkPrerequisitesOfAdvantageOrDisadvantage>[4],
getSelectOptionsById: (id: ActivatableIdentifier) => ResolvedSelectOption[] | undefined,
filterApplyingDependencies: FilterApplyingActivatableDependencies,
caps: {
startExperienceLevel?: Singleton.Static.ExperienceLevel
activeSermonsCount: number
activeVisionsCount: number
activeCantripsCount: number
activeSpellworksCount: number
activeMagicalActionsCount: number
activeBlessingsCount: number
activeLiturgicalChantsCount: number
activeCeremoniesCount: number
getDynamicSkillById: GetById.Dynamic.Skill
getDynamicLiturgicalChantById: GetById.Dynamic.LiturgicalChant
getDynamicCeremonyById: GetById.Dynamic.Ceremony
getDynamicSpellById: GetById.Dynamic.Spell
getDynamicRitualById: GetById.Dynamic.Ritual
getDynamicCloseCombatTechniqueById: GetById.Dynamic.CloseCombatTechnique
getDynamicRangedCombatTechniqueById: GetById.Dynamic.RangedCombatTechnique
},
): DisplayedActiveAdvantage[] =>
getActiveActivatables(
getStaticAdvantageById,
dynamicAdvantages,
(prerequisites, level, id) =>
checkPrerequisitesOfAdvantageOrDisadvantage(
prerequisites,
level,
"Advantage",
id,
prerequisiteCapabilities,
),
getSelectOptionsById,
getCreateIdentifierObject("Advantage"),
filterApplyingDependencies,
t10n => t10n.name,
caps,
)
@@ -0,0 +1,60 @@
import { Advantage } from "optolith-database-schema/types/Advantage"
import { Disadvantage } from "optolith-database-schema/types/Disadvantage"
import { MagicalTradition } from "optolith-database-schema/types/specialAbility/MagicalTradition"
import { Compare, compareAt } from "../../utils/compare.ts"
import { mapNullableDefault } from "../../utils/nullable.ts"
import { TranslateMap } from "../../utils/translate.ts"
import { DisplayedActiveActivatable } from "./activatableActive.ts"
import { DisplayedInactiveActivatable } from "./activatableInactive.ts"
/**
* Filters and sorts the displayed advantages or disadvantages.
*/
export const filterAndSortDisplayed = <
T extends
| DisplayedInactiveActivatable<Advantage | Disadvantage>
| DisplayedActiveActivatable<Advantage | Disadvantage>,
>(
visibleDisAdvantages: T[],
filterText: string,
translateMap: TranslateMap,
localeCompare: Compare<string>,
) => {
const getName = (c: T) => translateMap(c.static.translations)?.name ?? ""
return (
filterText === ""
? visibleDisAdvantages
: visibleDisAdvantages.filter(
c => getName(c).toLowerCase().includes(filterText.toLowerCase()) ?? false,
)
).sort(compareAt(getName, localeCompare))
}
/**
* The maximum number of adventure points that can be spent for advantages and
* disadvantages.
*/
export const maximumAdventurePointsForAdventagesAndDisadvantages = 80
/**
* Returns the maximum number of adventure points that can be spent for magical
* advantages and disadvantages.
*/
export const getMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages = (
activeMagicalTraditions: MagicalTradition[],
): number =>
activeMagicalTraditions.reduce(
(currentMax, tradition) =>
mapNullableDefault(
tradition.alternative_magical_adventure_points_maximum,
altMax => Math.min(altMax, currentMax),
currentMax,
),
50,
)
/**
* The maximum number of adventure points that can be spent for blessed
* advantages and disadvantages.
*/
export const maximumAdventurePointsForBlessedAdventagesAndDisadvantages = 50
@@ -0,0 +1,58 @@
import { ResolvedSelectOption } from "optolith-database-schema/cache/activatableSelectOptions"
import { Advantage } from "optolith-database-schema/types/Advantage"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { PublicationRefs } from "optolith-database-schema/types/source/_PublicationRef"
import { FilterApplyingActivatableDependencies } from "../dependencies/filterApplyingDependencies.ts"
import { All, GetById } from "../getTypes.ts"
import { getCreateIdentifierObject } from "../identifier.ts"
import {
checkPrerequisitesOfAdvantageOrDisadvantage,
checkPrerequisitesOfGeneralEntryWithLevels,
} from "../prerequisites/fullPrerequisiteValidationForType.ts"
import { DisplayedInactiveActivatable, getInactiveActivatables } from "./activatableInactive.ts"
/**
* A combination of a static advantage and its dynamic counterpart, extended by
* whether the entry can be activated and configuration options.
*/
export type DisplayedInactiveAdvantage = DisplayedInactiveActivatable<Advantage>
/**
* Returns all advantages with their corresponding dynamic entry, extended by
* whether the entry can be activated and configuration options.
*/
export const getInactiveAdvantages = (
staticAdvantages: All.Static.Advantages,
getDynamicAdvantageById: GetById.Dynamic.Advantage,
isEntryAvailable: (src: PublicationRefs) => boolean,
prerequisiteCapabilities: Parameters<typeof checkPrerequisitesOfAdvantageOrDisadvantage>[4],
getSelectOptionsById: (id: ActivatableIdentifier) => ResolvedSelectOption[] | undefined,
filterApplyingDependencies: FilterApplyingActivatableDependencies,
caps: {
activeSermonsCount: number
activeVisionsCount: number
activeSpellworksCount: number
getDynamicAdvantageById: GetById.Dynamic.Advantage
getDynamicDisadvantageById: GetById.Dynamic.Disadvantage
},
): DisplayedInactiveAdvantage[] =>
getInactiveActivatables(
staticAdvantages,
getDynamicAdvantageById,
isEntryAvailable,
(prerequisites, level, id) =>
checkPrerequisitesOfAdvantageOrDisadvantage(
prerequisites,
level,
"Advantage",
id,
prerequisiteCapabilities,
),
prerequisites =>
checkPrerequisitesOfGeneralEntryWithLevels(prerequisites, 1, prerequisiteCapabilities),
getSelectOptionsById,
getCreateIdentifierObject("Advantage"),
filterApplyingDependencies,
1,
caps,
)
@@ -0,0 +1,61 @@
import { ResolvedSelectOption } from "optolith-database-schema/cache/activatableSelectOptions"
import { Disadvantage } from "optolith-database-schema/types/Disadvantage"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { FilterApplyingActivatableDependencies } from "../dependencies/filterApplyingDependencies.ts"
import { All, GetById, Singleton } from "../getTypes.ts"
import { getCreateIdentifierObject } from "../identifier.ts"
import { checkPrerequisitesOfAdvantageOrDisadvantage } from "../prerequisites/fullPrerequisiteValidationForType.ts"
import { DisplayedActiveActivatable, getActiveActivatables } from "./activatableActive.ts"
/**
* A combination of a static disadvantage and its dynamic counterpart, extended by
* which activation/instance of the entry it represents.
*/
export type DisplayedActiveDisadvantage = DisplayedActiveActivatable<Disadvantage>
/**
* Returns all disadvantages with their corresponding dynamic entry, extended by
* which activation/instance of the entry it represents.
*/
export const getActiveDisadvantages = (
getStaticDisadvantageById: GetById.Static.Disadvantage,
dynamicDisadvantages: All.Dynamic.Disadvantages,
prerequisiteCapabilities: Parameters<typeof checkPrerequisitesOfAdvantageOrDisadvantage>[4],
getSelectOptionsById: (id: ActivatableIdentifier) => ResolvedSelectOption[] | undefined,
filterApplyingDependencies: FilterApplyingActivatableDependencies,
caps: {
startExperienceLevel?: Singleton.Static.ExperienceLevel
activeSermonsCount: number
activeVisionsCount: number
activeCantripsCount: number
activeSpellworksCount: number
activeMagicalActionsCount: number
activeBlessingsCount: number
activeLiturgicalChantsCount: number
activeCeremoniesCount: number
getDynamicSkillById: GetById.Dynamic.Skill
getDynamicLiturgicalChantById: GetById.Dynamic.LiturgicalChant
getDynamicCeremonyById: GetById.Dynamic.Ceremony
getDynamicSpellById: GetById.Dynamic.Spell
getDynamicRitualById: GetById.Dynamic.Ritual
getDynamicCloseCombatTechniqueById: GetById.Dynamic.CloseCombatTechnique
getDynamicRangedCombatTechniqueById: GetById.Dynamic.RangedCombatTechnique
},
): DisplayedActiveDisadvantage[] =>
getActiveActivatables(
getStaticDisadvantageById,
dynamicDisadvantages,
(prerequisites, level, id) =>
checkPrerequisitesOfAdvantageOrDisadvantage(
prerequisites,
level,
"Disadvantage",
id,
prerequisiteCapabilities,
),
getSelectOptionsById,
getCreateIdentifierObject("Disadvantage"),
filterApplyingDependencies,
t10n => t10n.name,
caps,
)
@@ -0,0 +1,58 @@
import { ResolvedSelectOption } from "optolith-database-schema/cache/activatableSelectOptions"
import { Disadvantage } from "optolith-database-schema/types/Disadvantage"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { PublicationRefs } from "optolith-database-schema/types/source/_PublicationRef"
import { FilterApplyingActivatableDependencies } from "../dependencies/filterApplyingDependencies.ts"
import { All, GetById } from "../getTypes.ts"
import { getCreateIdentifierObject } from "../identifier.ts"
import {
checkPrerequisitesOfAdvantageOrDisadvantage,
checkPrerequisitesOfGeneralEntryWithLevels,
} from "../prerequisites/fullPrerequisiteValidationForType.ts"
import { DisplayedInactiveActivatable, getInactiveActivatables } from "./activatableInactive.ts"
/**
* A combination of a static disadvantage and its dynamic counterpart, extended
* by whether the entry can be activated and configuration options.
*/
export type DisplayedInactiveDisadvantage = DisplayedInactiveActivatable<Disadvantage>
/**
* Returns all disadvantages with their corresponding dynamic entry, extended by
* whether the entry can be activated and configuration options.
*/
export const getInactiveDisadvantages = (
staticDisadvantages: All.Static.Disadvantages,
getDynamicDisadvantageById: GetById.Dynamic.Disadvantage,
isEntryAvailable: (src: PublicationRefs) => boolean,
prerequisiteCapabilities: Parameters<typeof checkPrerequisitesOfAdvantageOrDisadvantage>[4],
getSelectOptionsById: (id: ActivatableIdentifier) => ResolvedSelectOption[] | undefined,
filterApplyingDependencies: FilterApplyingActivatableDependencies,
caps: {
activeSermonsCount: number
activeVisionsCount: number
activeSpellworksCount: number
getDynamicAdvantageById: GetById.Dynamic.Advantage
getDynamicDisadvantageById: GetById.Dynamic.Disadvantage
},
): DisplayedInactiveDisadvantage[] =>
getInactiveActivatables(
staticDisadvantages,
getDynamicDisadvantageById,
isEntryAvailable,
(prerequisites, level, id) =>
checkPrerequisitesOfAdvantageOrDisadvantage(
prerequisites,
level,
"Disadvantage",
id,
prerequisiteCapabilities,
),
prerequisites =>
checkPrerequisitesOfGeneralEntryWithLevels(prerequisites, 1, prerequisiteCapabilities),
getSelectOptionsById,
getCreateIdentifierObject("Disadvantage"),
filterApplyingDependencies,
1,
caps,
)
@@ -1,5 +1,4 @@
import { MagicalTradition } from "optolith-database-schema/types/specialAbility/MagicalTradition"
import { mapNullableDefault } from "../../utils/nullable.ts"
import { Activatable, isActive } from "./activatableEntry.ts"
/**
@@ -23,20 +22,3 @@ export const isSpellcaster = (
spellcaster: Activatable | undefined,
magicalTraditions: Activatable[],
): boolean => isActive(spellcaster) && magicalTraditions.some(isActive)
/**
* Returns the maximum number of adventure points that can be spent for magical
* advantages and disadvantages.
*/
export const getMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages = (
activeMagicalTraditions: MagicalTradition[],
): number =>
activeMagicalTraditions.reduce(
(currentMax, tradition) =>
mapNullableDefault(
tradition.alternative_magical_adventure_points_maximum,
altMax => Math.min(altMax, currentMax),
currentMax,
),
50,
)
@@ -0,0 +1,40 @@
import { ActivatableOption } from "../activatable/activatableEntry.ts"
/**
* Bound adventure points are granted by the GM and can only be spent on the
* entry. They dont affect the costs of buying the activatable entry. They may
* specify for which configuration of an entry they count, just like a
* prerequisite.
*/
export type BoundAdventurePointsForActivatable = {
/**
* The granted adventure points.
*/
adventurePoints: number
/**
* The level for which the adventure points have been granted.
*/
level?: number
/**
* The set of options for which the adventure points have been granted.
*/
options?: ActivatableOption[]
}
/**
* 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
}
@@ -0,0 +1,40 @@
/**
* The accumulated used adventure points value for an entry. It is
* split by used bound and used general adventure points.
*/
export type AdventurePointsCache = {
/**
* The used general adventure points.
*/
general: number
/**
* The used bound adventure points.
*/
bound: number
}
/**
* An empty adventure points cache.
*/
export const emptyAdventurePointsCache: AdventurePointsCache = {
general: 0,
bound: 0,
}
/**
* Returns the total number of adventure points from the cache.
*/
export const getTotalAdventurePointsFromCache = (cache: AdventurePointsCache): number =>
cache.general + cache.bound
/**
* Adds two adventure points caches.
*/
export const addAdventurePointsCaches = (
a: AdventurePointsCache,
b: AdventurePointsCache,
): AdventurePointsCache => ({
general: a.general + b.general,
bound: a.bound + b.bound,
})
+71
View File
@@ -0,0 +1,71 @@
import {
ActivatableSelectOptionsCache,
ResolvedSelectOption,
} from "optolith-database-schema/cache/activatableSelectOptions"
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { assertExhaustive } from "../utils/typeSafety.ts"
/**
* Returns the select options for a given activatable identifier from the
* given cache.
*/
export const getSelectOptionsFromCacheById = (
cache: ActivatableSelectOptionsCache,
id: ActivatableIdentifier,
): ResolvedSelectOption[] | undefined => {
// prettier-ignore
switch (id.tag) {
case "Advantage": return cache.advantages[id.advantage]
case "Disadvantage": return cache.disadvantages[id.disadvantage]
case "GeneralSpecialAbility": return cache.generalSpecialAbilities[id.general_special_ability]
case "FatePointSpecialAbility": return cache.fatePointSpecialAbilities[id.fate_point_special_ability]
case "CombatSpecialAbility": return cache.combatSpecialAbilities[id.combat_special_ability]
case "MagicalSpecialAbility": return cache.magicalSpecialAbilities[id.magical_special_ability]
case "StaffEnchantment": return cache.staffEnchantments[id.staff_enchantment]
case "FamiliarSpecialAbility": return cache.familiarSpecialAbilities[id.familiar_special_ability]
case "KarmaSpecialAbility": return cache.karmaSpecialAbilities[id.karma_special_ability]
case "ProtectiveWardingCircleSpecialAbility": return cache.protectiveWardingCircleSpecialAbilities[id.protective_warding_circle_special_ability]
case "CombatStyleSpecialAbility": return cache.combatStyleSpecialAbilities[id.combat_style_special_ability]
case "AdvancedCombatSpecialAbility": return cache.advancedCombatSpecialAbilities[id.advanced_combat_special_ability]
case "CommandSpecialAbility": return cache.commandSpecialAbilities[id.command_special_ability]
case "MagicStyleSpecialAbility": return cache.magicStyleSpecialAbilities[id.magic_style_special_ability]
case "AdvancedMagicalSpecialAbility": return cache.advancedMagicalSpecialAbilities[id.advanced_magical_special_ability]
case "SpellSwordEnchantment": return cache.spellSwordEnchantments[id.spell_sword_enchantment]
case "DaggerRitual": return cache.daggerRituals[id.dagger_ritual]
case "InstrumentEnchantment": return cache.instrumentEnchantments[id.instrument_enchantment]
case "AttireEnchantment": return cache.attireEnchantments[id.attire_enchantment]
case "OrbEnchantment": return cache.orbEnchantments[id.orb_enchantment]
case "WandEnchantment": return cache.wandEnchantments[id.wand_enchantment]
case "BrawlingSpecialAbility": return cache.brawlingSpecialAbilities[id.brawling_special_ability]
case "AncestorGlyph": return cache.ancestorGlyphs[id.ancestor_glyph]
case "CeremonialItemSpecialAbility": return cache.ceremonialItemSpecialAbilities[id.ceremonial_item_special_ability]
case "Sermon": return cache.sermons[id.sermon]
case "LiturgicalStyleSpecialAbility": return cache.liturgicalStyleSpecialAbilities[id.liturgical_style_special_ability]
case "AdvancedKarmaSpecialAbility": return cache.advancedKarmaSpecialAbilities[id.advanced_karma_special_ability]
case "Vision": return cache.visions[id.vision]
case "MagicalTradition": return cache.magicalTraditions[id.magical_tradition]
case "BlessedTradition": return cache.blessedTraditions[id.blessed_tradition]
case "PactGift": return cache.pactGifts[id.pact_gift]
case "SikaryanDrainSpecialAbility": return cache.sikaryanDrainSpecialAbilities[id.sikaryan_drain_special_ability]
case "VampiricGift": return cache.vampiricGifts[id.vampiric_gift]
case "LycantropicGift": return cache.lycantropicGifts[id.lycantropic_gift]
case "SkillStyleSpecialAbility": return cache.skillStyleSpecialAbilities[id.skill_style_special_ability]
case "AdvancedSkillSpecialAbility": return cache.advancedSkillSpecialAbilities[id.advanced_skill_special_ability]
case "ArcaneOrbEnchantment": return cache.arcaneOrbEnchantments[id.arcane_orb_enchantment]
case "CauldronEnchantment": return cache.cauldronEnchantments[id.cauldron_enchantment]
case "FoolsHatEnchantment": return cache.foolsHatEnchantments[id.fools_hat_enchantment]
case "ToyEnchantment": return cache.toyEnchantments[id.toy_enchantment]
case "BowlEnchantment": return cache.bowlEnchantments[id.bowl_enchantment]
case "FatePointSexSpecialAbility": return cache.fatePointSexSpecialAbilities[id.fate_point_sex_special_ability]
case "SexSpecialAbility": return cache.sexSpecialAbilities[id.sex_special_ability]
case "WeaponEnchantment": return cache.weaponEnchantments[id.weapon_enchantment]
case "SickleRitual": return cache.sickleRituals[id.sickle_ritual]
case "RingEnchantment": return cache.ringEnchantments[id.ring_enchantment]
case "ChronicleEnchantment": return cache.chronicleEnchantments[id.chronicle_enchantment]
case "Krallenkettenzauber": return cache.krallenkettenzauber[id.krallenkettenzauber]
case "Trinkhornzauber": return cache.trinkhornzauber[id.trinkhornzauber]
// @ts-expect-error Magical Signs do not have select options; this is for forward compatibility
case "MagicalSign": return cache.magicalSigns?.[id.magical_sign]
default: assertExhaustive(id)
}
}
@@ -21,7 +21,15 @@ import {
ActivatableIdentifier,
SkillWithEnhancementsIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import { Preconditions } from "optolith-database-schema/types/prerequisites/ConditionalPrerequisites"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { ActivatableDependency } from "../activatable/activatableDependency.ts"
import { All, GetById } from "../getTypes.ts"
import { countMatchingRatedMinimumNumberEntries } from "../prerequisites/single/ratedMinimumNumberPrerequisiteValidation.ts"
import { sumMatchingRatedSumEntries } from "../prerequisites/single/ratedSumPrerequisiteValidation.ts"
import { GetDynamicLiturgicalChantsByAspectCapability } from "../rated/liturgicalChant.ts"
import { RatedDependency } from "../rated/ratedDependency.ts"
import { GetDynamicSpellworksByPropertyCapability } from "../rated/spell.ts"
/**
* A function that filters a list of dependencies by if they apply to the
@@ -32,11 +40,11 @@ import { RatedDependency } from "../rated/ratedDependency.ts"
export type FilterApplyingRatedDependencies = (dependencies: RatedDependency[]) => RatedDependency[]
/**
* `flattenDependencies` flattens the list of dependencies to usable values.
* That means, optional dependencies (objects) will be evaluated and will be
* included in the resulting list, depending on whether it has to follow the
* optional dependency or not. The result is a plain `List` of all non-optional
* dependencies.
* `filterApplyingRatedDependencies` flattens the list of dependencies to usable
* values. That means, optional dependencies (objects) will be evaluated and
* will be included in the resulting list, depending on whether it has to follow
* the optional dependency or not. The result is a plain `List` of all
* non-optional dependencies.
*/
export const filterApplyingRatedDependencies =
(
@@ -44,6 +52,15 @@ export const filterApplyingRatedDependencies =
id: ActivatableIdentifier | SkillWithEnhancementsIdentifier,
index: number,
) => boolean,
caps: {
getDynamicSkillById: GetById.Dynamic.Skill
dynamicCloseCombatTechniques: All.Dynamic.CloseCombatTechniques
dynamicRangedCombatTechniques: All.Dynamic.RangedCombatTechniques
getDynamicSpellsByProperty: GetDynamicSpellworksByPropertyCapability
getDynamicRitualsByProperty: GetDynamicSpellworksByPropertyCapability
getDynamicLiturgicalChantsByAspect: GetDynamicLiturgicalChantsByAspectCapability
getDynamicCeremoniesByAspect: GetDynamicLiturgicalChantsByAspectCapability
},
): FilterApplyingRatedDependencies =>
dependencies =>
dependencies.filter(dep => {
@@ -51,6 +68,56 @@ export const filterApplyingRatedDependencies =
return checkMultipleDisjunctionPartsAreValid(dep.source, dep.index)
}
switch (dep.value.tag) {
case "Fixed":
return true
case "MinimumNumberAtMinimumValue":
return (
countMatchingRatedMinimumNumberEntries(
dep.value.minimumValue,
dep.value.target,
caps,
) <= dep.value.minimumCount
)
case "Sum":
return sumMatchingRatedSumEntries(dep.value.targetIds, caps) <= dep.value.sum
default:
return assertExhaustive(dep.value)
}
})
/**
* A function that filters a list of dependencies by if they apply to the
* entry, since some dependencies target multiple entries but only one of them
* has to apply. And if other entries match the dependency, the dependency does
* not need to be matched by this entry.
*/
export type FilterApplyingActivatableDependencies = (
dependencies: ActivatableDependency[],
) => ActivatableDependency[]
/**
* `filterApplyingActivatableDependencies` flattens the list of dependencies to
* usable values. That means, optional dependencies (objects) will be evaluated
* and will be included in the resulting list, depending on whether it has to
* follow the optional dependency or not. The result is a plain `List` of all
* non-optional dependencies.
*/
export const filterApplyingActivatableDependencies =
(
checkMultipleDisjunctionPartsAreValid: (id: ActivatableIdentifier, index: number) => boolean,
checkPreconditions: (prerequisites: Preconditions) => boolean,
): FilterApplyingActivatableDependencies =>
dependencies =>
dependencies.filter(dep => {
if (dep.when !== undefined && !checkPreconditions(dep.when)) {
return false
}
if (dep.isPartOfDisjunction) {
return checkMultipleDisjunctionPartsAreValid(dep.sourceId, dep.index)
}
return true
})
@@ -1,20 +1,189 @@
import { Draft } from "@reduxjs/toolkit"
import {
AdvantageIdentifier,
DisadvantageIdentifier,
} from "optolith-database-schema/types/_Identifier"
import {
LiturgyIdentifier,
SpecialAbilityIdentifier,
SpellworkIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import {
AdvantageDisadvantagePrerequisites,
GeneralPrerequisites,
LiturgyPrerequisites,
PlainGeneralPrerequisites,
SpellworkPrerequisites,
} from "optolith-database-schema/types/_Prerequisite"
import { RangeBounds } from "../../utils/range.ts"
import { Character } from "../character.ts"
import { registerOrUnregisterPlainPrerequisites } from "./prerequisiteCombinatorRegistrationAsDependency.ts"
import {
registerOrUnregisterPlainPrerequisites,
registerOrUnregisterPrerequisitesForLevels,
} from "./prerequisiteCombinatorRegistrationAsDependency.ts"
import {
registerOrUnregisterPrerequisiteOfAdvantageOrDisadvantageAsDependency,
registerOrUnregisterPrerequisiteOfGeneralAsDependency,
registerOrUnregisterPrerequisiteOfLiturgyAsDependency,
registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
} from "./prerequisiteRegistrationAsDependencyForType.ts"
import { RegistrationMethod } from "./registrationHelpers.ts"
/**
* Register or unregister all prerequisites of a general entry as dependencies
* on the character's draft.
*/
export const registerOrUnregisterPrerequisitesOfPlainGeneralAsDependencies = (
method: RegistrationMethod,
character: Draft<Character>,
p: PlainGeneralPrerequisites,
sourceId: SpecialAbilityIdentifier,
capabilities: {
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
},
): void =>
registerOrUnregisterPlainPrerequisites(
method,
character,
p,
sourceId,
registerOrUnregisterPrerequisiteOfGeneralAsDependency,
capabilities,
)
/**
* Register or unregister all prerequisites of a general entry as dependencies
* on the character's draft.
*/
export const registerOrUnregisterPrerequisitesOfGeneralAsDependencies = (
method: RegistrationMethod,
character: Draft<Character>,
p: GeneralPrerequisites,
sourceId: SpecialAbilityIdentifier,
levelRange: RangeBounds,
capabilities: {
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
},
): void =>
registerOrUnregisterPrerequisitesForLevels(
method,
character,
p,
levelRange,
sourceId,
registerOrUnregisterPrerequisiteOfGeneralAsDependency,
capabilities,
)
/**
* Register or unregister all prerequisites of a profession as dependencies on
* the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfProfessionAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: ProfessionPrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of an advantage or disadvantage as
* dependencies on the character's draft.
*/
export const registerOrUnregisterPrerequisitesOfAdvantageOrDisadvantageAsDependencies = (
method: RegistrationMethod,
character: Draft<Character>,
p: AdvantageDisadvantagePrerequisites,
sourceId: AdvantageIdentifier | DisadvantageIdentifier,
levelRange: RangeBounds,
capabilities: {
ancestorBloodAdvantageIds: number[]
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
},
): void =>
registerOrUnregisterPrerequisitesForLevels(
method,
character,
p,
levelRange,
sourceId,
registerOrUnregisterPrerequisiteOfAdvantageOrDisadvantageAsDependency,
capabilities,
)
/**
* Register or unregister all prerequisites of an arcane tradition as
* dependencies on the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfArcaneTraditionAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: ArcaneTraditionPrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of a personality trait as
* dependencies on the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfPersonalityTraitAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: PersonalityTraitPrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of a spellwork as dependencies on
* the character's draft.
@@ -50,3 +219,93 @@ export const registerOrUnregisterPrerequisitesOfLiturgyAsDependencies = (
sourceId,
registerOrUnregisterPrerequisiteOfLiturgyAsDependency,
)
/**
* Register or unregister all prerequisites of an influence as dependencies on
* the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfInfluenceAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: InfluencePrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of a language as dependencies on the
* character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfLanguageAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: LanguagePrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of an animist power as dependencies
* on the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfAnimistPowerAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: AnimistPowerPrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of a geode ritual as dependencies on
* the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfGeodeRitualAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: GeodeRitualPrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
/**
* Register or unregister all prerequisites of an enhancement as dependencies on
* the character's draft.
*/
// export const registerOrUnregisterPrerequisitesOfEnhancementAsDependencies = (
// method: RegistrationMethod,
// character: Draft<Character>,
// p: EnhancementPrerequisites,
// sourceId: SpellworkIdentifier,
// ): void =>
// registerOrUnregisterPlainPrerequisites(
// method,
// character,
// p,
// sourceId,
// registerOrUnregisterPrerequisiteOfSpellworkAsDependency,
// )
@@ -7,41 +7,57 @@ import {
PrerequisitesElement,
PrerequisitesForLevels,
} from "optolith-database-schema/types/_Prerequisite"
import { RangeBounds, isInRange } from "../../utils/range.ts"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { Character } from "../character.ts"
import { RegistrationFunction, RegistrationMethod } from "./registrationHelpers.ts"
const registerOrUnregisterPrerequisiteGroup = <T, SourceId>(
const registerOrUnregisterPrerequisiteGroup = <T, SourceId, C>(
method: RegistrationMethod,
character: Draft<Character>,
group: PrerequisiteGroup<T>,
sourceId: SourceId,
index: number,
registerOrUnregister: RegistrationFunction<T, SourceId>,
registerOrUnregister: RegistrationFunction<T, SourceId, C>,
...rest: C extends undefined ? [] : [capabilities: C]
): void =>
group.list.forEach(p => registerOrUnregister(method, character, p, sourceId, index, false))
group.list.forEach(p =>
registerOrUnregister(method, character, p, sourceId, index, false, ...rest),
)
const registerOrUnregisterPrerequisiteDisjunction = <T, SourceId>(
const registerOrUnregisterPrerequisiteDisjunction = <T, SourceId, C>(
method: RegistrationMethod,
character: Draft<Character>,
disjunction: PrerequisitesDisjunction<T>,
sourceId: SourceId,
index: number,
registerOrUnregister: RegistrationFunction<T, SourceId>,
registerOrUnregister: RegistrationFunction<T, SourceId, C>,
...rest: C extends undefined ? [] : [capabilities: C]
): void =>
disjunction.list.forEach(p => registerOrUnregister(method, character, p, sourceId, index, true))
disjunction.list.forEach(p =>
registerOrUnregister(method, character, p, sourceId, index, true, ...rest),
)
const registerOrUnregisterPrerequisiteElement = <T, SourceId>(
const registerOrUnregisterPrerequisiteElement = <T, SourceId, C>(
method: RegistrationMethod,
character: Draft<Character>,
element: PrerequisitesElement<T>,
sourceId: SourceId,
index: number,
registerOrUnregister: RegistrationFunction<T, SourceId>,
registerOrUnregister: RegistrationFunction<T, SourceId, C>,
...rest: C extends undefined ? [] : [capabilities: C]
): void => {
switch (element.tag) {
case "Single":
return registerOrUnregister(method, character, element.single, sourceId, index, false)
return registerOrUnregister(
method,
character,
element.single,
sourceId,
index,
false,
...rest,
)
case "Group":
return registerOrUnregisterPrerequisiteGroup(
method,
@@ -50,6 +66,7 @@ const registerOrUnregisterPrerequisiteElement = <T, SourceId>(
sourceId,
index,
registerOrUnregister,
...rest,
)
case "Disjunction":
return registerOrUnregisterPrerequisiteDisjunction(
@@ -59,6 +76,7 @@ const registerOrUnregisterPrerequisiteElement = <T, SourceId>(
sourceId,
index,
registerOrUnregister,
...rest,
)
default:
return assertExhaustive(element)
@@ -68,12 +86,13 @@ const registerOrUnregisterPrerequisiteElement = <T, SourceId>(
/**
* Checks a plain list of prerequisites if theyre matched.
*/
export const registerOrUnregisterPlainPrerequisites = <T, SourceId>(
export const registerOrUnregisterPlainPrerequisites = <T, SourceId, C = undefined>(
method: RegistrationMethod,
character: Draft<Character>,
prerequisites: PlainPrerequisites<T>,
sourceId: SourceId,
registerOrUnregister: RegistrationFunction<T, SourceId>,
registerOrUnregister: RegistrationFunction<T, SourceId, C>,
...rest: C extends undefined ? [] : [capabilities: C]
): void =>
prerequisites.forEach((p, index) =>
registerOrUnregisterPrerequisiteElement(
@@ -83,19 +102,21 @@ export const registerOrUnregisterPlainPrerequisites = <T, SourceId>(
sourceId,
index,
registerOrUnregister,
...rest,
),
)
const registerOrUnregisterPrerequisiteForLevel = <T, SourceId>(
const registerOrUnregisterPrerequisiteForLevel = <T, SourceId, C>(
method: RegistrationMethod,
character: Draft<Character>,
prerequisiteForLevel: PrerequisiteForLevel<T>,
level: number,
levelRange: RangeBounds,
sourceId: SourceId,
index: number,
registerOrUnregister: RegistrationFunction<T, SourceId>,
registerOrUnregister: RegistrationFunction<T, SourceId, C>,
...rest: C extends undefined ? [] : [capabilities: C]
): void => {
if (prerequisiteForLevel.level <= level) {
if (isInRange(levelRange, prerequisiteForLevel.level)) {
registerOrUnregisterPrerequisiteElement(
method,
character,
@@ -103,6 +124,7 @@ const registerOrUnregisterPrerequisiteForLevel = <T, SourceId>(
sourceId,
index,
registerOrUnregister,
...rest,
)
}
}
@@ -111,22 +133,24 @@ const registerOrUnregisterPrerequisiteForLevel = <T, SourceId>(
* Checks a list of level-specific prerequisites if theyre matched up to a
* given level.
*/
export const registerOrUnregisterPrerequisitesForLevels = <T, SourceId>(
export const registerOrUnregisterPrerequisitesForLevels = <T, SourceId, C = undefined>(
method: RegistrationMethod,
character: Draft<Character>,
prerequisitesForLevels: PrerequisitesForLevels<T>,
level: number,
levelRange: RangeBounds,
sourceId: SourceId,
registerOrUnregister: RegistrationFunction<T, SourceId>,
registerOrUnregister: RegistrationFunction<T, SourceId, C>,
...rest: C extends undefined ? [] : [capabilities: C]
): void =>
prerequisitesForLevels.forEach((p, index) =>
registerOrUnregisterPrerequisiteForLevel(
method,
character,
p,
level,
levelRange,
sourceId,
index,
registerOrUnregister,
...rest,
),
)
@@ -1,15 +1,460 @@
import {
AdvantageIdentifier,
DisadvantageIdentifier,
} from "optolith-database-schema/types/_Identifier"
import {
LiturgyIdentifier,
SpecialAbilityIdentifier,
SpellworkIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import {
AdvantageDisadvantagePrerequisiteGroup,
GeneralPrerequisiteGroup,
LiturgyPrerequisiteGroup,
SpellworkPrerequisiteGroup,
} from "optolith-database-schema/types/prerequisites/PrerequisiteGroups"
import { assertExhaustive } from "../../utils/typeSafety.ts"
import { RegistrationFunction } from "./registrationHelpers.ts"
import { registerOrUnregisterActivatablePrerequisiteAsDependency } from "./single/activatablePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterNoOtherAncestorBloodAdvantagePrerequisiteAsDependency } from "./single/ancestorBloodAdvantagePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterCommonSuggestedByRCPPrerequisiteAsDependency } from "./single/commonSuggestedByRCPPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterCulturePrerequisiteAsDependency } from "./single/culturePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterExternalEnhancementPrerequisiteAsDependency } from "./single/enhancementPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterPactPrerequisiteAsDependency } from "./single/pactPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterPrimaryAttributePrerequisiteAsDependency } from "./single/primaryAttributePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterRacePrerequisiteAsDependency } from "./single/racePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterRatedMinimumNumberPrerequisiteAsDependency } from "./single/ratedMinimumNumberPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterRatedPrerequisiteAsDependency } from "./single/ratedPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterRatedSumPrerequisiteAsDependency } from "./single/ratedSumPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterRulePrerequisiteAsDependency } from "./single/rulePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterSexPrerequisiteAsDependency } from "./single/sexPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterSexualCharacteristicPrerequisiteAsDependency } from "./single/sexualCharacteristicPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterSocialStatusPrerequisiteAsDependency } from "./single/socialStatusPrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterStatePrerequisiteAsDependency } from "./single/statePrerequisiteRegistrationAsDependency.ts"
import { registerOrUnregisterTextPrerequisiteAsDependency } from "./single/textPrerequisiteRegistrationAsDependency.ts"
import {
registerOrUnregisterBlessedTraditionPrerequisiteAsDependency,
registerOrUnregisterMagicalTraditionPrerequisiteAsDependency,
} from "./single/traditionPrerequisiteRegistrationAsDependency.ts"
/**
* Registers or unregisters a prerequisite of a general entry as a dependency on
* the character's draft.
*/
export const registerOrUnregisterPrerequisiteOfGeneralAsDependency: RegistrationFunction<
GeneralPrerequisiteGroup,
SpecialAbilityIdentifier,
{
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
}
> = (method, character, p, sourceId, index, isPartOfDisjunction, capabilities): void => {
switch (p.tag) {
case "Sex":
return registerOrUnregisterSexPrerequisiteAsDependency(
method,
character,
p.sex,
sourceId,
index,
isPartOfDisjunction,
)
case "Race":
return registerOrUnregisterRacePrerequisiteAsDependency(
method,
character,
p.race,
sourceId,
index,
isPartOfDisjunction,
)
case "Culture":
return registerOrUnregisterCulturePrerequisiteAsDependency(
method,
character,
p.culture,
sourceId,
index,
isPartOfDisjunction,
)
case "Pact":
return registerOrUnregisterPactPrerequisiteAsDependency(
method,
character,
p.pact,
sourceId,
index,
isPartOfDisjunction,
)
case "SocialStatus":
return registerOrUnregisterSocialStatusPrerequisiteAsDependency(
method,
character,
p.social_status,
sourceId,
index,
isPartOfDisjunction,
)
case "State":
return registerOrUnregisterStatePrerequisiteAsDependency(
method,
character,
p.state,
sourceId,
index,
isPartOfDisjunction,
)
case "Rule":
return registerOrUnregisterRulePrerequisiteAsDependency(
method,
character,
p.rule,
sourceId,
index,
isPartOfDisjunction,
)
case "PrimaryAttribute":
return registerOrUnregisterPrimaryAttributePrerequisiteAsDependency(
method,
character,
p.primary_attribute,
sourceId,
index,
isPartOfDisjunction,
)
case "Activatable":
return registerOrUnregisterActivatablePrerequisiteAsDependency(
method,
character,
p.activatable,
sourceId,
index,
isPartOfDisjunction,
)
case "BlessedTradition":
return registerOrUnregisterBlessedTraditionPrerequisiteAsDependency(
method,
character,
p.blessed_tradition,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "MagicalTradition":
return registerOrUnregisterMagicalTraditionPrerequisiteAsDependency(
method,
character,
p.magical_tradition,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "Rated":
return registerOrUnregisterRatedPrerequisiteAsDependency(
method,
character,
p.rated,
sourceId,
index,
isPartOfDisjunction,
)
case "RatedMinimumNumber":
return registerOrUnregisterRatedMinimumNumberPrerequisiteAsDependency(
method,
character,
p.rated_minimum_number,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "RatedSum":
return registerOrUnregisterRatedSumPrerequisiteAsDependency(
method,
character,
p.rated_sum,
sourceId,
index,
isPartOfDisjunction,
)
case "ExternalEnhancement":
return registerOrUnregisterExternalEnhancementPrerequisiteAsDependency(
method,
character,
p.external_enhancement,
sourceId,
index,
isPartOfDisjunction,
)
case "Text":
return registerOrUnregisterTextPrerequisiteAsDependency(
method,
character,
p.text,
sourceId,
index,
isPartOfDisjunction,
)
case "SexualCharacteristic":
return registerOrUnregisterSexualCharacteristicPrerequisiteAsDependency(
method,
character,
p.sexual_characteristic,
sourceId,
index,
isPartOfDisjunction,
)
default:
return assertExhaustive(p)
}
}
/**
* Registers or unregisters a prerequisite of a profession as a dependency on
* the character's draft.
*/
// export const registerOrUnregisterPrerequisiteOfProfessionAsDependency: RegistrationFunction<ProfessionPrerequisiteGroup, ProfessionIdentifier> = (method, character, p, sourceId, index, isPartOfDisjunction, capabilities): void => {
// switch (p.tag) {
// default:
// return assertExhaustive(p)
// }
// }
/**
* Registers or unregisters a prerequisite of an advantage or disadvantage as a
* dependency on the character's draft.
*/
export const registerOrUnregisterPrerequisiteOfAdvantageOrDisadvantageAsDependency: RegistrationFunction<
AdvantageDisadvantagePrerequisiteGroup,
AdvantageIdentifier | DisadvantageIdentifier,
{
ancestorBloodAdvantageIds: number[]
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
}
> = (method, character, p, sourceId, index, isPartOfDisjunction, capabilities): void => {
switch (p.tag) {
case "CommonSuggestedByRCP":
return registerOrUnregisterCommonSuggestedByRCPPrerequisiteAsDependency(
method,
character,
p.common_suggested_by_rcp,
sourceId,
index,
isPartOfDisjunction,
)
case "Sex":
return registerOrUnregisterSexPrerequisiteAsDependency(
method,
character,
p.sex,
sourceId,
index,
isPartOfDisjunction,
)
case "Race":
return registerOrUnregisterRacePrerequisiteAsDependency(
method,
character,
p.race,
sourceId,
index,
isPartOfDisjunction,
)
case "Culture":
return registerOrUnregisterCulturePrerequisiteAsDependency(
method,
character,
p.culture,
sourceId,
index,
isPartOfDisjunction,
)
case "Pact":
return registerOrUnregisterPactPrerequisiteAsDependency(
method,
character,
p.pact,
sourceId,
index,
isPartOfDisjunction,
)
case "SocialStatus":
return registerOrUnregisterSocialStatusPrerequisiteAsDependency(
method,
character,
p.social_status,
sourceId,
index,
isPartOfDisjunction,
)
case "State":
return registerOrUnregisterStatePrerequisiteAsDependency(
method,
character,
p.state,
sourceId,
index,
isPartOfDisjunction,
)
case "Rule":
return registerOrUnregisterRulePrerequisiteAsDependency(
method,
character,
p.rule,
sourceId,
index,
isPartOfDisjunction,
)
case "PrimaryAttribute":
return registerOrUnregisterPrimaryAttributePrerequisiteAsDependency(
method,
character,
p.primary_attribute,
sourceId,
index,
isPartOfDisjunction,
)
case "Activatable":
return registerOrUnregisterActivatablePrerequisiteAsDependency(
method,
character,
p.activatable,
sourceId,
index,
isPartOfDisjunction,
)
case "BlessedTradition":
return registerOrUnregisterBlessedTraditionPrerequisiteAsDependency(
method,
character,
p.blessed_tradition,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "MagicalTradition":
return registerOrUnregisterMagicalTraditionPrerequisiteAsDependency(
method,
character,
p.magical_tradition,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "Rated":
return registerOrUnregisterRatedPrerequisiteAsDependency(
method,
character,
p.rated,
sourceId,
index,
isPartOfDisjunction,
)
case "RatedMinimumNumber":
return registerOrUnregisterRatedMinimumNumberPrerequisiteAsDependency(
method,
character,
p.rated_minimum_number,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "RatedSum":
return registerOrUnregisterRatedSumPrerequisiteAsDependency(
method,
character,
p.rated_sum,
sourceId,
index,
isPartOfDisjunction,
)
case "ExternalEnhancement":
return registerOrUnregisterExternalEnhancementPrerequisiteAsDependency(
method,
character,
p.external_enhancement,
sourceId,
index,
isPartOfDisjunction,
)
case "Text":
return registerOrUnregisterTextPrerequisiteAsDependency(
method,
character,
p.text,
sourceId,
index,
isPartOfDisjunction,
)
case "NoOtherAncestorBloodAdvantage":
return registerOrUnregisterNoOtherAncestorBloodAdvantagePrerequisiteAsDependency(
method,
character,
p.no_other_ancestor_blood_advantage,
sourceId,
index,
isPartOfDisjunction,
capabilities,
)
case "SexualCharacteristic":
return registerOrUnregisterSexualCharacteristicPrerequisiteAsDependency(
method,
character,
p.sexual_characteristic,
sourceId,
index,
isPartOfDisjunction,
)
default:
return assertExhaustive(p)
}
}
/**
* Registers or unregisters a prerequisite of a spellwork as a dependency on the
* character's draft.
*/
// export const registerOrUnregisterPrerequisiteOfArcaneTraditionAsDependency: RegistrationFunction<ArcaneTraditionPrerequisiteGroup, ArcaneTraditionIdentifier> = (method, character, p, sourceId, index, isPartOfDisjunction, capabilities): void => {
// switch (p.tag) {
// default:
// return assertExhaustive(p)
// }
// }
/**
* Registers or unregisters a prerequisite of a spellwork as a dependency on the
* character's draft.
*/
// export const registerOrUnregisterPrerequisiteOfPersonalityTraitAsDependency: RegistrationFunction<PersonalityTraitPrerequisiteGroup, PersonalityTraitIdentifier> = (method, character, p, sourceId, index, isPartOfDisjunction, capabilities): void => {
// switch (p.tag) {
// default:
// return assertExhaustive(p)
// }
// }
/**
* Registers or unregisters a prerequisite of a spellwork as a dependency on the
@@ -1,6 +1,6 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { RegistrationMethod, addOrRemoveDependency } from "./registrationHelpers.ts"
import { RegistrationMethod, addOrRemoveDependencyInSlice } from "./registrationHelpers.ts"
describe("addOrRemoveDependency", () => {
const createDefault = () => ({ dependencies: [] })
@@ -9,7 +9,7 @@ describe("addOrRemoveDependency", () => {
const slice: Record<string, { dependencies: { name: string }[] }> = {}
const id = "testId"
const dep = { name: "dependency" }
addOrRemoveDependency(RegistrationMethod.Add, slice, id, createDefault, dep)
addOrRemoveDependencyInSlice(RegistrationMethod.Add, slice, id, createDefault, dep)
assert.deepEqual(slice[id]?.dependencies, [dep])
})
@@ -21,7 +21,7 @@ describe("addOrRemoveDependency", () => {
dependencies: [dep1, dep2],
},
}
addOrRemoveDependency(RegistrationMethod.Remove, slice, "testId", createDefault, dep1)
addOrRemoveDependencyInSlice(RegistrationMethod.Remove, slice, "testId", createDefault, dep1)
assert.deepEqual(slice["testId"]?.dependencies, [dep2])
})
@@ -33,7 +33,7 @@ describe("addOrRemoveDependency", () => {
dependencies: [dep1],
},
}
addOrRemoveDependency(RegistrationMethod.Remove, slice, "testId", createDefault, dep2)
addOrRemoveDependencyInSlice(RegistrationMethod.Remove, slice, "testId", createDefault, dep2)
assert.deepEqual(slice["testId"].dependencies, [dep1])
})
})
@@ -12,34 +12,24 @@ export enum RegistrationMethod {
}
/**
* Adds or removes a dependency from a character slice. When adding a
* dependency and no entry exists for the given id, a new entry is created
* using the given `createDefault` function. Removing a dependency is done by
* a deep comparison of the dependency objects.
* Adds or removes a dependency from a dependency array. Removing a dependency
* is done by a deep comparison of the dependency objects.
*/
export const addOrRemoveDependency = <
K extends string | number | symbol,
D,
T extends { dependencies: D[] },
>(
export const addOrRemoveDependency = <D>(
method: RegistrationMethod,
slice: Record<K, T>,
id: K,
createDefault: (id: K) => T,
dependencies: D[],
dep: D,
): void => {
switch (method) {
case RegistrationMethod.Add: {
const entry = (slice[id] ??= createDefault(id))
entry.dependencies.push(dep)
dependencies.push(dep)
break
}
case RegistrationMethod.Remove: {
const entry = slice[id]
const index = entry?.dependencies.findIndex(d => deepEqual(d, dep)) ?? -1
const index = dependencies.findIndex(d => deepEqual(d, dep)) ?? -1
if (index > -1) {
entry?.dependencies.splice(index, 1)
dependencies.splice(index, 1)
}
break
}
@@ -49,15 +39,55 @@ export const addOrRemoveDependency = <
}
}
/**
* Adds or removes a dependency from a character slice. When adding a
* dependency and no entry exists for the given identifier, a new entry is
* created using the given `createDefault` function. Removing a dependency is
* done by a deep comparison of the dependency objects.
*/
export const addOrRemoveDependencyInSlice = <
K extends string | number | symbol,
D,
T extends { dependencies: D[] },
>(
method: RegistrationMethod,
slice: Record<K, T>,
id: K,
createDefault: (id: K) => T,
dep: D,
): void => {
const entry = (slice[id] ??= createDefault(id))
addOrRemoveDependency(method, entry.dependencies, dep)
}
/**
* A function that registers or unregisters a prerequisite as a dependency on
* the character's draft.
*/
export type RegistrationFunction<T, SourceId> = (
export type RegistrationFunction<T, SourceId, C = undefined> = (
method: RegistrationMethod,
character: Draft<Character>,
p: T,
sourceId: SourceId,
index: number,
isPartOfDisjunction: boolean,
...rest: C extends undefined ? [] : [capabilities: C]
) => void
/**
* Additional data some registration functions need.
*/
export type RegistrationFunctionCapabilities = {
ancestorBloodAdvantageIds: number[]
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
}
@@ -0,0 +1,430 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { ActivatablePrerequisite } from "optolith-database-schema/types/prerequisites/single/ActivatablePrerequisite"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { ActivatableDependency } from "../../activatable/activatableDependency.ts"
import { createEmptyDynamicActivatable } from "../../activatable/activatableEntry.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link ActivatablePrerequisite} as a dependency on
* the character's draft.
*/
export const registerOrUnregisterActivatablePrerequisiteAsDependency: RegistrationFunction<
ActivatablePrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: ActivatableDependency = {
sourceId,
index,
isPartOfDisjunction,
active: p.active,
level: p.level,
options: p.options,
when: p.when,
}
switch (p.id.tag) {
case "Advantage":
return addOrRemoveDependencyInSlice(
method,
character.advantages,
p.id.advantage,
createEmptyDynamicActivatable,
dependency,
)
case "Disadvantage":
return addOrRemoveDependencyInSlice(
method,
character.disadvantages,
p.id.disadvantage,
createEmptyDynamicActivatable,
dependency,
)
case "GeneralSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.generalSpecialAbilities,
p.id.general_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "FatePointSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.fatePointSpecialAbilities,
p.id.fate_point_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "CombatSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.combatSpecialAbilities,
p.id.combat_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "MagicalSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicalSpecialAbilities,
p.id.magical_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "StaffEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.staffEnchantments,
p.id.staff_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "FamiliarSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.familiarSpecialAbilities,
p.id.familiar_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "KarmaSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.karmaSpecialAbilities,
p.id.karma_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "ProtectiveWardingCircleSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.protectiveWardingCircleSpecialAbilities,
p.id.protective_warding_circle_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "CombatStyleSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.combatStyleSpecialAbilities,
p.id.combat_style_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "AdvancedCombatSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.advancedCombatSpecialAbilities,
p.id.advanced_combat_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "CommandSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.commandSpecialAbilities,
p.id.command_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "MagicStyleSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicStyleSpecialAbilities,
p.id.magic_style_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "AdvancedMagicalSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.advancedMagicalSpecialAbilities,
p.id.advanced_magical_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "SpellSwordEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.spellSwordEnchantments,
p.id.spell_sword_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "DaggerRitual":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.daggerRituals,
p.id.dagger_ritual,
createEmptyDynamicActivatable,
dependency,
)
case "InstrumentEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.instrumentEnchantments,
p.id.instrument_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "AttireEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.attireEnchantments,
p.id.attire_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "OrbEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.orbEnchantments,
p.id.orb_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "WandEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.wandEnchantments,
p.id.wand_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "BrawlingSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.brawlingSpecialAbilities,
p.id.brawling_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "AncestorGlyph":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.ancestorGlyphs,
p.id.ancestor_glyph,
createEmptyDynamicActivatable,
dependency,
)
case "CeremonialItemSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.ceremonialItemSpecialAbilities,
p.id.ceremonial_item_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "Sermon":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.sermons,
p.id.sermon,
createEmptyDynamicActivatable,
dependency,
)
case "LiturgicalStyleSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.liturgicalStyleSpecialAbilities,
p.id.liturgical_style_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "AdvancedKarmaSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.advancedKarmaSpecialAbilities,
p.id.advanced_karma_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "Vision":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.visions,
p.id.vision,
createEmptyDynamicActivatable,
dependency,
)
case "MagicalTradition":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicalTraditions,
p.id.magical_tradition,
createEmptyDynamicActivatable,
dependency,
)
case "BlessedTradition":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.blessedTraditions,
p.id.blessed_tradition,
createEmptyDynamicActivatable,
dependency,
)
case "PactGift":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.pactGifts,
p.id.pact_gift,
createEmptyDynamicActivatable,
dependency,
)
case "VampiricGift":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.vampiricGifts,
p.id.vampiric_gift,
createEmptyDynamicActivatable,
dependency,
)
case "SikaryanDrainSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.sikaryanDrainSpecialAbilities,
p.id.sikaryan_drain_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "LycantropicGift":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.lycantropicGifts,
p.id.lycantropic_gift,
createEmptyDynamicActivatable,
dependency,
)
case "SkillStyleSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.skillStyleSpecialAbilities,
p.id.skill_style_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "AdvancedSkillSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.advancedSkillSpecialAbilities,
p.id.advanced_skill_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "ArcaneOrbEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.arcaneOrbEnchantments,
p.id.arcane_orb_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "CauldronEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.cauldronEnchantments,
p.id.cauldron_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "FoolsHatEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.foolsHatEnchantments,
p.id.fools_hat_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "ToyEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.toyEnchantments,
p.id.toy_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "BowlEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.bowlEnchantments,
p.id.bowl_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "FatePointSexSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.fatePointSexSpecialAbilities,
p.id.fate_point_sex_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "SexSpecialAbility":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.sexSpecialAbilities,
p.id.sex_special_ability,
createEmptyDynamicActivatable,
dependency,
)
case "WeaponEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.weaponEnchantments,
p.id.weapon_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "SickleRitual":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.sickleRituals,
p.id.sickle_ritual,
createEmptyDynamicActivatable,
dependency,
)
case "RingEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.ringEnchantments,
p.id.ring_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "ChronicleEnchantment":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.chronicleEnchantments,
p.id.chronicle_enchantment,
createEmptyDynamicActivatable,
dependency,
)
case "Krallenkettenzauber":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.krallenkettenzauber,
p.id.krallenkettenzauber,
createEmptyDynamicActivatable,
dependency,
)
case "Trinkhornzauber":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.trinkhornzauber,
p.id.trinkhornzauber,
createEmptyDynamicActivatable,
dependency,
)
case "MagicalSign":
return addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicalSigns,
p.id.magical_sign,
createEmptyDynamicActivatable,
dependency,
)
default:
return assertExhaustive(p.id)
}
}
@@ -0,0 +1,42 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { AncestorBloodPrerequisite } from "optolith-database-schema/types/prerequisites/single/AncestorBloodPrerequisite"
import { ActivatableDependency } from "../../activatable/activatableDependency.ts"
import { createEmptyDynamicActivatable } from "../../activatable/activatableEntry.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link AncestorBloodPrerequisite} as a dependency
* on the character's draft.
*/
export const registerOrUnregisterNoOtherAncestorBloodAdvantagePrerequisiteAsDependency: RegistrationFunction<
AncestorBloodPrerequisite,
ActivatableIdentifier,
{ ancestorBloodAdvantageIds: number[] }
> = (
method,
character,
_p,
sourceId,
index,
isPartOfDisjunction,
{ ancestorBloodAdvantageIds },
): void => {
const dependency: ActivatableDependency = {
sourceId,
index,
isPartOfDisjunction,
active: false,
}
ancestorBloodAdvantageIds.forEach(id => {
if (sourceId.tag !== "Advantage" || sourceId.advantage !== id) {
addOrRemoveDependencyInSlice(
method,
character.advantages,
id,
createEmptyDynamicActivatable,
dependency,
)
}
})
}
@@ -0,0 +1,14 @@
import { AnimistPowerIdentifier } from "optolith-database-schema/types/_Identifier"
import { AnimistPowerPrerequisite } from "optolith-database-schema/types/prerequisites/single/AnimistPowerPrerequisite"
import { RegistrationFunction } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link AnimistPowerPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterAnimistPowerPrerequisiteAsDependency: RegistrationFunction<
AnimistPowerPrerequisite,
AnimistPowerIdentifier
> = (): void => {
// TODO: Missing implementation
}
@@ -0,0 +1,16 @@
import {
AdvantageIdentifier,
DisadvantageIdentifier,
} from "optolith-database-schema/types/_Identifier"
import { RegistrationFunction } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link CommonSuggestedByRCPPrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterCommonSuggestedByRCPPrerequisiteAsDependency: RegistrationFunction<
Record<string, never>,
AdvantageIdentifier | DisadvantageIdentifier
> = (): void => {
// no op
}
@@ -0,0 +1,22 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { CulturePrerequisite } from "optolith-database-schema/types/prerequisites/single/CulturePrerequisite"
import { CultureDependency } from "../../culture.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link CulturePrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterCulturePrerequisiteAsDependency: RegistrationFunction<
CulturePrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: CultureDependency = {
sourceId,
index,
isPartOfDisjunction,
id: p.id.culture,
}
addOrRemoveDependency(method, character.culture.dependencies, dependency)
}
@@ -0,0 +1,80 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import {
ExternalEnhancementPrerequisite,
InternalEnhancementPrerequisite,
} from "optolith-database-schema/types/prerequisites/single/EnhancementPrerequisite"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { EnhancementDependency } from "../../rated/enhancement.ts"
import { createEmptyDynamicLiturgicalChant } from "../../rated/liturgicalChant.ts"
import { createEmptyDynamicSpell } from "../../rated/spell.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters an {@link ExternalEnhancementPrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterExternalEnhancementPrerequisiteAsDependency: RegistrationFunction<
ExternalEnhancementPrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: EnhancementDependency = {
tag: "External",
sourceId,
index,
isPartOfDisjunction,
}
const addOrRemoveDependencyInSlice = <
K extends string | number | symbol,
T extends { enhancements: Record<number, { dependencies: EnhancementDependency[] }> },
>(
slice: Record<K, T>,
id: K,
createDefault: (id: K) => T,
): void => {
const entry = (slice[id] ??= createDefault(id))
if (Object.hasOwn(entry.enhancements, p.enhancement.id)) {
addOrRemoveDependency(method, entry.enhancements[p.enhancement.id]!.dependencies, dependency)
}
}
switch (p.skill.id.tag) {
case "Spell":
return addOrRemoveDependencyInSlice(
character.spells,
p.skill.id.spell,
createEmptyDynamicSpell,
)
case "Ritual":
return addOrRemoveDependencyInSlice(
character.rituals,
p.skill.id.ritual,
createEmptyDynamicSpell,
)
case "LiturgicalChant":
return addOrRemoveDependencyInSlice(
character.liturgicalChants,
p.skill.id.liturgical_chant,
createEmptyDynamicLiturgicalChant,
)
case "Ceremony":
return addOrRemoveDependencyInSlice(
character.ceremonies,
p.skill.id.ceremony,
createEmptyDynamicLiturgicalChant,
)
default:
return assertExhaustive(p.skill.id)
}
}
/**
* Registers or unregisters an {@link InternalEnhancementPrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterInfluencePrerequisiteAsDependency: RegistrationFunction<
InternalEnhancementPrerequisite,
number
> = (): void => {
// TODO: Missing implementation
}
@@ -0,0 +1,14 @@
import { InfluenceIdentifier } from "optolith-database-schema/types/_Identifier"
import { InfluencePrerequisite } from "optolith-database-schema/types/prerequisites/single/InfluencePrerequisite"
import { RegistrationFunction } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link InfluencePrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterInfluencePrerequisiteAsDependency: RegistrationFunction<
InfluencePrerequisite,
InfluenceIdentifier
> = (): void => {
// TODO: Missing implementation
}
@@ -0,0 +1,24 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { PactPrerequisite } from "optolith-database-schema/types/prerequisites/single/PactPrerequisite"
import { PactDependency } from "../../pact.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link PactPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterPactPrerequisiteAsDependency: RegistrationFunction<
PactPrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: PactDependency = {
sourceId,
index,
isPartOfDisjunction,
category: p.category,
domain: p.domain_id,
level: p.level,
}
addOrRemoveDependency(method, character.pactDependencies, dependency)
}
@@ -0,0 +1,41 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { PrimaryAttributePrerequisite } from "optolith-database-schema/types/prerequisites/single/PrimaryAttributePrerequisite"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { PrimaryAttributeDependency } from "../../rated/primaryAttribute.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link PrimaryAttributePrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterPrimaryAttributePrerequisiteAsDependency: RegistrationFunction<
PrimaryAttributePrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: PrimaryAttributeDependency = {
sourceId,
index,
isPartOfDisjunction,
value: {
tag: "Minimum",
minimum: p.value,
},
}
switch (p.category) {
case "Blessed":
return addOrRemoveDependency(
method,
character.blessedPrimaryAttributeDependencies,
dependency,
)
case "Magical":
return addOrRemoveDependency(
method,
character.magicalPrimaryAttributeDependencies,
dependency,
)
default:
return assertExhaustive(p.category)
}
}
@@ -0,0 +1,22 @@
import { PublicationIdentifier } from "optolith-database-schema/types/_Identifier"
import { PublicationPrerequisite } from "optolith-database-schema/types/prerequisites/single/PublicationPrerequisite"
import { PublicationDependency } from "../../sources/publicationDependency.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link PublicationPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterPublicationPrerequisiteAsDependency: RegistrationFunction<
PublicationPrerequisite,
PublicationIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: PublicationDependency = {
sourceId,
index,
isPartOfDisjunction,
id: p.id,
}
addOrRemoveDependency(method, character.rules.publicationDependencies, dependency)
}
@@ -0,0 +1,23 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { RacePrerequisite } from "optolith-database-schema/types/prerequisites/single/RacePrerequisite"
import { RaceDependency } from "../../race.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link RacePrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterRacePrerequisiteAsDependency: RegistrationFunction<
RacePrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: RaceDependency = {
sourceId,
index,
isPartOfDisjunction,
id: p.id.race,
active: p.active,
}
addOrRemoveDependency(method, character.race.dependencies, dependency)
}
@@ -0,0 +1,153 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { RatedMinimumNumberPrerequisite } from "optolith-database-schema/types/prerequisites/single/RatedMinimumNumberPrerequisite"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { createEmptyDynamicCombatTechnique } from "../../rated/combatTechnique.ts"
import { createEmptyDynamicLiturgicalChant } from "../../rated/liturgicalChant.ts"
import { RatedDependency } from "../../rated/ratedDependency.ts"
import { createEmptyDynamicSkill } from "../../rated/skill.ts"
import { createEmptyDynamicSpell } from "../../rated/spell.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link RatedMinimumNumberPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterRatedMinimumNumberPrerequisiteAsDependency: RegistrationFunction<
RatedMinimumNumberPrerequisite,
ActivatableIdentifier,
{
closeCombatTechniqueIds: number[]
rangedCombatTechniqueIds: number[]
getSpellIdsByPropertyId: (id: number) => number[]
getRitualIdsByPropertyId: (id: number) => number[]
getLiturgicalChantIdsByAspectId: (id: number) => number[]
getCeremonyIdsByAspectId: (id: number) => number[]
}
> = (
method,
character,
p,
sourceId,
index,
isPartOfDisjunction,
{
closeCombatTechniqueIds,
rangedCombatTechniqueIds,
getSpellIdsByPropertyId,
getRitualIdsByPropertyId,
getLiturgicalChantIdsByAspectId,
getCeremonyIdsByAspectId,
},
): void => {
const dependency: RatedDependency = {
source: sourceId,
index,
isPartOfDisjunction,
value: {
tag: "MinimumNumberAtMinimumValue",
minimumCount: p.number,
minimumValue: p.value,
target: p.targets,
},
}
switch (p.targets.tag) {
case "Skills":
return p.targets.skills.list.forEach(ref =>
addOrRemoveDependencyInSlice(
method,
character.skills,
ref.id.skill,
createEmptyDynamicSkill,
dependency,
),
)
case "CombatTechniques":
switch (p.targets.combat_techniques.group) {
case "All":
closeCombatTechniqueIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.combatTechniques.close,
id,
createEmptyDynamicCombatTechnique,
dependency,
),
)
rangedCombatTechniqueIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.combatTechniques.ranged,
id,
createEmptyDynamicCombatTechnique,
dependency,
),
)
return
case "Close":
return closeCombatTechniqueIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.combatTechniques.close,
id,
createEmptyDynamicCombatTechnique,
dependency,
),
)
case "Ranged":
return rangedCombatTechniqueIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.combatTechniques.ranged,
id,
createEmptyDynamicCombatTechnique,
dependency,
),
)
default:
return assertExhaustive(p.targets.combat_techniques.group)
}
case "Spellworks":
getSpellIdsByPropertyId(p.targets.spellworks.id.property).forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.spells,
id,
createEmptyDynamicSpell,
dependency,
),
)
getRitualIdsByPropertyId(p.targets.spellworks.id.property).forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.rituals,
id,
createEmptyDynamicSpell,
dependency,
),
)
return
case "Liturgies":
getLiturgicalChantIdsByAspectId(p.targets.liturgies.id.aspect).forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.liturgicalChants,
id,
createEmptyDynamicLiturgicalChant,
dependency,
),
)
getCeremonyIdsByAspectId(p.targets.liturgies.id.aspect).forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.ceremonies,
id,
createEmptyDynamicLiturgicalChant,
dependency,
),
)
return
default:
return assertExhaustive(p.targets)
}
}
@@ -17,7 +17,7 @@ describe(registerOrUnregisterRatedPrerequisiteAsDependency.name, () => {
dependencies: [
{
source: { tag: "Spell", spell: 1 },
value: { tag: "Minimum", minimum: 10 },
value: { tag: "Fixed", value: { tag: "Minimum", minimum: 10 } },
index: 0,
isPartOfDisjunction: false,
},
@@ -60,7 +60,7 @@ describe(registerOrUnregisterRatedPrerequisiteAsDependency.name, () => {
dependencies: [
{
source: { tag: "Spell", spell: 1 },
value: { tag: "Minimum", minimum: 10 },
value: { tag: "Fixed", value: { tag: "Minimum", minimum: 10 } },
index: 0,
isPartOfDisjunction: false,
},
@@ -93,13 +93,13 @@ describe(registerOrUnregisterRatedPrerequisiteAsDependency.name, () => {
dependencies: [
{
source: { tag: "Spell", spell: 1 },
value: { tag: "Minimum", minimum: 10 },
value: { tag: "Fixed", value: { tag: "Minimum", minimum: 10 } },
index: 0,
isPartOfDisjunction: false,
},
{
source: { tag: "Ritual", ritual: 1 },
value: { tag: "Minimum", minimum: 11 },
value: { tag: "Fixed", value: { tag: "Minimum", minimum: 11 } },
index: 0,
isPartOfDisjunction: false,
},
@@ -116,7 +116,7 @@ describe(registerOrUnregisterRatedPrerequisiteAsDependency.name, () => {
dependencies: [
{
source: { tag: "Ritual", ritual: 1 },
value: { tag: "Minimum", minimum: 11 },
value: { tag: "Fixed", value: { tag: "Minimum", minimum: 11 } },
index: 0,
isPartOfDisjunction: false,
},
@@ -10,10 +10,11 @@ import { createEmptyDynamicLiturgicalChant } from "../../rated/liturgicalChant.t
import { RatedDependency } from "../../rated/ratedDependency.ts"
import { createEmptyDynamicSkill } from "../../rated/skill.ts"
import { createEmptyDynamicSpell } from "../../rated/spell.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link RatedPrerequisite} as a dependency on the character's draft.
* Registers or unregisters a {@link RatedPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunction<
RatedPrerequisite,
@@ -24,14 +25,17 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
index,
isPartOfDisjunction,
value: {
tag: "Minimum",
minimum: p.value,
tag: "Fixed",
value: {
tag: "Minimum",
minimum: p.value,
},
},
}
switch (p.id.tag) {
case "Attribute":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.attributes,
p.id.attribute,
@@ -39,7 +43,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "Skill":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.skills,
p.id.skill,
@@ -47,7 +51,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "CloseCombatTechnique":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.combatTechniques.close,
p.id.close_combat_technique,
@@ -55,7 +59,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "RangedCombatTechnique":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.combatTechniques.ranged,
p.id.ranged_combat_technique,
@@ -63,7 +67,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "Spell":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.spells,
p.id.spell,
@@ -71,7 +75,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "Ritual":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.rituals,
p.id.ritual,
@@ -79,7 +83,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "LiturgicalChant":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.liturgicalChants,
p.id.liturgical_chant,
@@ -87,7 +91,7 @@ export const registerOrUnregisterRatedPrerequisiteAsDependency: RegistrationFunc
dependency,
)
case "Ceremony":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.ceremonies,
p.id.ceremony,
@@ -0,0 +1,38 @@
import {
ActivatableIdentifier,
SkillWithEnhancementsIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import { RatedSumPrerequisite } from "optolith-database-schema/types/prerequisites/single/RatedSumPrerequisite"
import { RatedDependency } from "../../rated/ratedDependency.ts"
import { createEmptyDynamicSkill } from "../../rated/skill.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link RatedSumPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterRatedSumPrerequisiteAsDependency: RegistrationFunction<
RatedSumPrerequisite,
ActivatableIdentifier | SkillWithEnhancementsIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: RatedDependency = {
source: sourceId,
index,
isPartOfDisjunction,
value: {
tag: "Sum",
sum: p.sum,
targetIds: p.targets,
},
}
p.targets.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.skills,
id.skill,
createEmptyDynamicSkill,
dependency,
),
)
}
@@ -7,10 +7,11 @@ import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { createFocusRule } from "../../rules/focusRule.ts"
import { createOptionalRule } from "../../rules/optionalRule.ts"
import { RuleDependency } from "../../rules/ruleDependency.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link RulePrerequisite} as a dependency on the character's draft.
* Registers or unregisters a {@link RulePrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterRulePrerequisiteAsDependency: RegistrationFunction<
RulePrerequisite,
@@ -24,7 +25,7 @@ export const registerOrUnregisterRulePrerequisiteAsDependency: RegistrationFunct
switch (p.id.tag) {
case "FocusRule":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.rules.focusRules,
p.id.focus_rule,
@@ -32,7 +33,7 @@ export const registerOrUnregisterRulePrerequisiteAsDependency: RegistrationFunct
dependency,
)
case "OptionalRule":
return addOrRemoveDependency(
return addOrRemoveDependencyInSlice(
method,
character.rules.optionalRules,
p.id.optional_rule,
@@ -0,0 +1,25 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { SexPrerequisite } from "optolith-database-schema/types/prerequisites/single/SexPrerequisite"
import { SexDependency } from "../../sex.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link SexPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterSexPrerequisiteAsDependency: RegistrationFunction<
SexPrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: SexDependency = {
value: {
kind: "BinarySex",
value: p.id,
},
sourceId,
index,
isPartOfDisjunction,
}
addOrRemoveDependency(method, character.personalData.sexDependencies, dependency)
}
@@ -0,0 +1,25 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { SexualCharacteristicPrerequisite } from "optolith-database-schema/types/prerequisites/single/SexualCharacteristicPrerequisite"
import { SexDependency } from "../../sex.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link SexualCharacteristicPrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterSexualCharacteristicPrerequisiteAsDependency: RegistrationFunction<
SexualCharacteristicPrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: SexDependency = {
value: {
kind: "SexualCharacteristic",
value: p.id,
},
sourceId,
index,
isPartOfDisjunction,
}
addOrRemoveDependency(method, character.personalData.sexDependencies, dependency)
}
@@ -0,0 +1,22 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { SocialStatusPrerequisite } from "optolith-database-schema/types/prerequisites/single/SocialStatusPrerequisite"
import { SocialStatusDependency } from "../../socialStatus.ts"
import { RegistrationFunction, addOrRemoveDependency } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link SocialStatusPrerequisite} as a dependency
* on the character's draft.
*/
export const registerOrUnregisterSocialStatusPrerequisiteAsDependency: RegistrationFunction<
SocialStatusPrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: SocialStatusDependency = {
id: p.id.social_status,
sourceId,
index,
isPartOfDisjunction,
}
addOrRemoveDependency(method, character.personalData.socialStatus.dependencies, dependency)
}
@@ -0,0 +1,21 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import { StatePrerequisite } from "optolith-database-schema/types/prerequisites/single/StatePrerequisite"
import { StateDependency, createState } from "../../state.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link StatePrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterStatePrerequisiteAsDependency: RegistrationFunction<
StatePrerequisite,
ActivatableIdentifier
> = (method, character, p, sourceId, index, isPartOfDisjunction): void => {
const dependency: StateDependency = {
sourceId,
index,
isPartOfDisjunction,
}
addOrRemoveDependencyInSlice(method, character.states, p.id.state, createState, dependency)
}
@@ -0,0 +1,17 @@
import {
ActivatableIdentifier,
SkillWithEnhancementsIdentifier,
} from "optolith-database-schema/types/_IdentifierGroup"
import { TextPrerequisite } from "optolith-database-schema/types/prerequisites/single/TextPrerequisite"
import { RegistrationFunction } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link TextPrerequisite} as a dependency on the
* character's draft.
*/
export const registerOrUnregisterTextPrerequisiteAsDependency: RegistrationFunction<
TextPrerequisite,
ActivatableIdentifier | SkillWithEnhancementsIdentifier
> = (): void => {
// no op
}
@@ -0,0 +1,140 @@
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
import {
BlessedTraditionPrerequisite,
MagicalTraditionPrerequisite,
} from "optolith-database-schema/types/prerequisites/single/TraditionPrerequisite"
import { assertExhaustive } from "../../../utils/typeSafety.ts"
import { ActivatableDependency } from "../../activatable/activatableDependency.ts"
import { createEmptyDynamicActivatable } from "../../activatable/activatableEntry.ts"
import { RegistrationFunction, addOrRemoveDependencyInSlice } from "../registrationHelpers.ts"
/**
* Registers or unregisters a {@link BlessedTraditionPrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterBlessedTraditionPrerequisiteAsDependency: RegistrationFunction<
BlessedTraditionPrerequisite,
ActivatableIdentifier,
{
blessedTraditionChurchIds: number[]
blessedTraditionShamanisticIds: number[]
}
> = (
method,
character,
p,
sourceId,
index,
_isPartOfDisjunction,
{ blessedTraditionChurchIds, blessedTraditionShamanisticIds },
): void => {
const dependency: ActivatableDependency = {
sourceId,
index,
isPartOfDisjunction: true,
active: true,
}
switch (p.restriction) {
case "Church":
return blessedTraditionChurchIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.specialAbilities.blessedTraditions,
id,
createEmptyDynamicActivatable,
dependency,
),
)
case "Shamanistic":
return blessedTraditionShamanisticIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.specialAbilities.blessedTraditions,
id,
createEmptyDynamicActivatable,
dependency,
),
)
case undefined:
return [...blessedTraditionChurchIds, ...blessedTraditionShamanisticIds].forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.specialAbilities.blessedTraditions,
id,
createEmptyDynamicActivatable,
dependency,
),
)
default:
return assertExhaustive(p.restriction)
}
}
/**
* Registers or unregisters a {@link MagicalTraditionPrerequisite} as a
* dependency on the character's draft.
*/
export const registerOrUnregisterMagicalTraditionPrerequisiteAsDependency: RegistrationFunction<
MagicalTraditionPrerequisite,
ActivatableIdentifier,
{
magicalTraditionIds: number[]
magicalTraditionIdsThatCanLearnRituals: number[]
magicalTraditionIdsThatCanBindFamiliars: number[]
}
> = (
method,
character,
p,
sourceId,
index,
_isPartOfDisjunction,
{
magicalTraditionIds,
magicalTraditionIdsThatCanLearnRituals,
magicalTraditionIdsThatCanBindFamiliars,
},
): void => {
const dependency: ActivatableDependency = {
sourceId,
index,
isPartOfDisjunction: true,
active: true,
}
switch (p.restriction) {
case "CanLearnRituals":
return magicalTraditionIdsThatCanLearnRituals.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicalTraditions,
id,
createEmptyDynamicActivatable,
dependency,
),
)
case "CanBindFamiliars":
return magicalTraditionIdsThatCanBindFamiliars.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicalTraditions,
id,
createEmptyDynamicActivatable,
dependency,
),
)
case undefined:
return magicalTraditionIds.forEach(id =>
addOrRemoveDependencyInSlice(
method,
character.specialAbilities.magicalTraditions,
id,
createEmptyDynamicActivatable,
dependency,
),
)
default:
return assertExhaustive(p.restriction)
}
}
+16 -2
View File
@@ -153,9 +153,11 @@ import { config as animalShapePathConfig } from "optolith-database-schema/types/
import { config as animalShapeSizeConfig } from "optolith-database-schema/types/traditionArtifacts/sub/AnimalShapeSize"
import { config as brewConfig } from "optolith-database-schema/types/traditionArtifacts/sub/Brew"
import { Activatable, TinyActivatable } from "./activatable/activatableEntry.ts"
import { Pact as PactInstance } from "./pact.ts"
import { ActivatableRated, ActivatableRatedWithEnhancements, Rated } from "./rated/ratedEntry.ts"
import { FocusRuleInstance } from "./rules/focusRule.ts"
import { OptionalRuleInstance } from "./rules/optionalRule.ts"
import { Sex as SexInstance } from "./sex.ts"
import { StateInstance } from "./state.ts"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -409,7 +411,7 @@ export namespace GetById {
// export type MagicalArtifact = GetByIdDynamic<typeof magicalArtifactConfig, never>
export type MagicalDance = GetByIdDynamic<typeof magicalDanceConfig, ActivatableRated>
export type MagicalMelody = GetByIdDynamic<typeof magicalMelodyConfig, ActivatableRated>
export type MagicalRune = GetByIdDynamic<typeof magicalRuneConfig, Activatable>
export type MagicalRune = GetByIdDynamic<typeof magicalRuneConfig, ActivatableRated>
export type MagicalSign = GetByIdDynamic<typeof magicalSignConfig, Activatable>
export type MagicalSpecialAbility = GetByIdDynamic<typeof magicalSpecialAbilityConfig, Activatable>
export type MagicalTradition = GetByIdDynamic<typeof magicalTraditionConfig, Activatable>
@@ -456,6 +458,18 @@ export namespace GetById {
}
}
// prettier-ignore
export namespace Singleton {
export namespace Static {
export type ExperienceLevel = TypeFromConfig<typeof experienceLevelConfig> | undefined
}
export namespace Dynamic {
export type Pact = PactInstance | undefined
export type Sex = SexInstance
}
}
// prettier-ignore
export namespace All {
export namespace Static {
@@ -679,7 +693,7 @@ export namespace All {
// export type MagicalArtifacts = GetAllDynamic<typeof magicalArtifactConfig, never>
export type MagicalDances = AllDynamic<typeof magicalDanceConfig, ActivatableRated>
export type MagicalMelodies = AllDynamic<typeof magicalMelodyConfig, ActivatableRated>
export type MagicalRunes = AllDynamic<typeof magicalRuneConfig, Activatable>
export type MagicalRunes = AllDynamic<typeof magicalRuneConfig, ActivatableRated>
export type MagicalSigns = AllDynamic<typeof magicalSignConfig, Activatable>
export type MagicalSpecialAbilities = AllDynamic<typeof magicalSpecialAbilityConfig, Activatable>
export type MagicalTraditions = AllDynamic<typeof magicalTraditionConfig, Activatable>
@@ -100,6 +100,7 @@ export const checkMultipleDisjunctionPartsAreValid =
getStaticMagicalTraditionById: GetById.Static.MagicalTradition
getStaticBlessedTraditionById: GetById.Static.BlessedTradition
getStaticPactGiftById: GetById.Static.PactGift
getStaticVampiricGiftById: GetById.Static.VampiricGift
getStaticSikaryanDrainSpecialAbilityById: GetById.Static.SikaryanDrainSpecialAbility
getStaticLycantropicGiftById: GetById.Static.LycantropicGift
getStaticSkillStyleSpecialAbilityById: GetById.Static.SkillStyleSpecialAbility
@@ -117,6 +118,7 @@ export const checkMultipleDisjunctionPartsAreValid =
getStaticChronicleEnchantmentById: GetById.Static.ChronicleEnchantment
getStaticKrallenkettenzauberById: GetById.Static.Krallenkettenzauber
getStaticTrinkhornzauberById: GetById.Static.Trinkhornzauber
getStaticMagicalSignById: GetById.Static.MagicalSign
getStaticSpellById: GetById.Static.Spell
getStaticRitualById: GetById.Static.Ritual
getStaticLiturgicalChantById: GetById.Static.LiturgicalChant
@@ -351,6 +353,13 @@ export const checkMultipleDisjunctionPartsAreValid =
index,
caps,
)
case "VampiricGift":
return checkLevelPrerequisite(
checkPrerequisiteOfGeneralEntry,
caps.getStaticVampiricGiftById(source.vampiric_gift),
index,
caps,
)
case "SikaryanDrainSpecialAbility":
return checkLevelPrerequisite(
checkPrerequisiteOfGeneralEntry,
@@ -470,6 +479,13 @@ export const checkMultipleDisjunctionPartsAreValid =
index,
caps,
)
case "MagicalSign":
return checkLevelPrerequisite(
checkPrerequisiteOfGeneralEntry,
caps.getStaticMagicalSignById(source.magical_sign),
index,
caps,
)
case "Spell":
return checkPlainPrerequisite(
checkPrerequisiteOfSpellwork,
@@ -21,6 +21,7 @@ import {
PublicationPrerequisites,
SpellworkPrerequisites,
} from "optolith-database-schema/types/_Prerequisite"
import { Preconditions } from "optolith-database-schema/types/prerequisites/ConditionalPrerequisites"
import {
checkPlainPrerequisites,
checkPrerequisitesForLevels,
@@ -196,3 +197,11 @@ export const checkPrerequisitesOfEnhancement = (
checkPlainPrerequisites(prerequisites, p =>
SingleChecks.checkPrerequisiteOfEnhancement(capabilities, p),
)
/**
* Checks all prerequisites for an enhancement.
*/
export const checkPreconditions = (
prerequisites: Preconditions,
capabilities: Parameters<typeof SingleChecks.checkPrecondition>[0],
): boolean => prerequisites.every(p => SingleChecks.checkPrecondition(capabilities, p))

Some files were not shown because too many files have changed in this diff Show More