diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 96b6e24f..bfc771b6 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -135,136 +135,6 @@ rules: no-shadow-restricted-names: 2 # no-undef: 2 - # Stylistic Issues - array-bracket-spacing: - - 2 - - always - array-element-newline: - - 2 - - consistent - block-spacing: 2 - # capitalized-comments: 2 - comma-dangle: - - 2 - - arrays: always-multiline - objects: always-multiline - imports: always-multiline - exports: always-multiline - functions: only-multiline - comma-style: 2 - computed-property-spacing: 2 - eol-last: 2 - func-name-matching: 2 - func-names: 2 - func-style: - - 2 - - declaration - - allowArrowFunctions: true - function-call-argument-newline: - - 2 - - consistent - function-paren-newline: - - 2 - - multiline-arguments - jsx-quotes: 2 - key-spacing: 2 - keyword-spacing: 2 - lines-around-comment: - - 0 - # requires an empty line before block comments - - beforeBlockComment: true - # requires an empty line after block comments - afterBlockComment: false - # requires an empty line before line comments - beforeLineComment: true - # requires an empty line after line comments - afterLineComment: false - # allows comments to appear at the start of block statements - allowBlockStart: true - # allows comments to appear at the end of block statements - allowBlockEnd: false - # allows comments to appear at the start of object literals - allowObjectStart: true - # allows comments to appear at the end of object literals - allowObjectEnd: false - # allows comments to appear at the start of array literals - allowArrayStart: true - # allows comments to appear at the end of array literals - allowArrayEnd: false - # allows comments to appear at the start of classes - allowClassStart: true - # allows comments to appear at the end of classes - allowClassEnd: false - ignorePattern: "^ *@" - lines-between-class-members: 2 - max-len: - - 2 - - code: 100 - ignoreComments: true - ignoreTemplateLiterals: true - ignorePattern: "^import |^export \\{(.*?)\\}|^ \\* |^ *it ?\\(\"\\w+|^export const \\w+ = createAction(?:<.+?>)?\\(\"\\w+/\\w+\"\\)" - max-statements-per-line: [2, { max: 2 }] - multiline-ternary: - - 2 - - always-multiline - new-parens: 2 - newline-per-chained-call: - - 2 - - ignoreChainWithDepth: 2 - no-bitwise: 2 - no-continue: 2 - no-lonely-if: 2 - no-mixed-operators: - - 2 - - groups: - - ["&", "|", "^", "~", "<<", ">>", ">>>"] - - ["==", "!=", "===", "!==", ">", ">=", "<", "<="] - - ["&&", "||"] - - ["in", "instanceof"] - allowSamePrecedence: true - no-mixed-spaces-and-tabs: 2 - no-multiple-empty-lines: 2 - no-negated-condition: 2 - no-new-object: 2 - no-tabs: 2 - no-trailing-spaces: 2 - no-unneeded-ternary: 2 - nonblock-statement-body-position: 2 - # object-curly-newline: - # - 2 - # - multiline: true - object-curly-spacing: - - 2 - - always - operator-assignment: 2 - operator-linebreak: - - 2 - - before - - overrides: - "=": ignore - padded-blocks: - - 2 - - blocks: never - classes: never - switches: never - prefer-object-spread: 2 - quote-props: - - 2 - - as-needed - semi-spacing: 2 - semi-style: 2 - space-before-blocks: 2 - space-in-parens: 2 - space-infix-ops: - - 2 - - int32Hint: true - space-unary-ops: 2 - spaced-comment: 2 - switch-colon-spacing: 2 - template-tag-spacing: - - 2 - - always - # ECMAScript 6 arrow-body-style: 2 arrow-parens: @@ -272,9 +142,7 @@ rules: - as-needed arrow-spacing: 2 constructor-super: 2 - generator-star-spacing: - - 2 - - after + generator-star-spacing: error no-class-assign: 2 no-new-symbol: 2 no-this-before-super: 2 @@ -354,46 +222,17 @@ rules: "@typescript-eslint/unified-signatures": 2 # TypeScript Extension - brace-style: 0 - "@typescript-eslint/brace-style": - - 2 - - stroustrup - comma-spacing: 0 - "@typescript-eslint/comma-spacing": 2 - func-call-spacing: 0 - "@typescript-eslint/func-call-spacing": 2 - indent: 0 - "@typescript-eslint/indent": 0 no-array-constructor: 0 "@typescript-eslint/no-array-constructor": 2 - no-extra-parens: 0 - "@typescript-eslint/no-extra-parens": 0 - no-extra-semi: 0 - "@typescript-eslint/no-extra-semi": 2 no-unused-expressions: 0 "@typescript-eslint/no-redeclare": 2 "@typescript-eslint/no-shadow": 2 "@typescript-eslint/no-unused-expressions": 2 - "@typescript-eslint/no-unused-vars": - - 2 - - argsIgnorePattern: "^_" - varsIgnorePattern: "^_" "@typescript-eslint/no-use-before-define": 2 no-useless-constructor: 0 "@typescript-eslint/no-useless-constructor": 2 - quotes: 0 - "@typescript-eslint/quotes": - - 2 - - double - - avoidEscape: true - allowTemplateLiterals: true no-return-await: 0 "@typescript-eslint/return-await": 2 - semi: 0 - "@typescript-eslint/semi": - - 2 - - never - - beforeStatementContinuationChars: never # React react/boolean-prop-naming: 2 @@ -434,9 +273,9 @@ rules: - 2 - never react/jsx-child-element-spacing: 2 - react/jsx-closing-bracket-location: - - 2 - - props-aligned + # react/jsx-closing-bracket-location: + # - 2 + # - props-aligned react/jsx-closing-tag-location: 2 react/jsx-curly-newline: 2 react/jsx-curly-spacing: 2 diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..c5d38556 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,5 @@ +arrowParens: avoid +printWidth: 100 +semi: false +tabWidth: 2 +trailingComma: all diff --git a/.vscode/settings.json b/.vscode/settings.json index e66dd20a..00e302ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,11 @@ "eslint.lintTask.enable": true, "eslint.lintTask.options": "\"src/**\" -c \"./.eslintrc.yaml\" --ext=\"ts\" --ext=\"tsx\" --ignore-pattern=\"./src/App/Utilities/YAML/Schema/**\"", "editor.tabSize": 2, - "editor.detectIndentation": false + "editor.detectIndentation": false, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, } diff --git a/src/App/Containers/AttributesContainer.ts b/src/App/Containers/AttributesContainer.ts deleted file mode 100644 index d81b7ba9..00000000 --- a/src/App/Containers/AttributesContainer.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { connect } from "react-redux" -import { fromJust, isJust, Maybe } from "../../Data/Maybe" -import { ReduxDispatch } from "../Actions/Actions" -import * as AttributesActions from "../Actions/AttributesActions" -import * as SubwindowsActions from "../Actions/SubwindowsActions" -import { EnergyId } from "../Constants/Ids" -import { AppStateRecord } from "../Models/AppState" -import { getAdjustmentValue, getAttributesForView, getAttributeSum, getAvailableAdjustmentIds } from "../Selectors/attributeSelectors" -import { getDerivedCharacteristics } from "../Selectors/derivedCharacteristicsSelectors" -import { getMaxTotalAttributeValues } from "../Selectors/elSelectors" -import { getIsInCharacterCreation, getIsRemovingEnabled } from "../Selectors/phaseSelectors" -import { getAddPermanentEnergy, getCurrentAttributeAdjustmentId, getEditPermanentEnergy } from "../Selectors/stateSelectors" -import { Attributes, AttributesDispatchProps, AttributesOwnProps, AttributesStateProps } from "../Views/Attributes/Attributes" - -const mapStateToProps = - (state: AppStateRecord, ownProps: AttributesOwnProps): AttributesStateProps => ({ - adjustmentValue: getAdjustmentValue (state, ownProps), - attributes: getAttributesForView (state, ownProps), - availableAttributeIds: getAvailableAdjustmentIds (state, ownProps), - currentAttributeId: getCurrentAttributeAdjustmentId (state), - isInCharacterCreation: getIsInCharacterCreation (state), - isRemovingEnabled: getIsRemovingEnabled (state), - derived: getDerivedCharacteristics (state, ownProps), - maxTotalAttributeValues: getMaxTotalAttributeValues (state), - sum: getAttributeSum (state, ownProps), - getEditPermanentEnergy: getEditPermanentEnergy (state), - getAddPermanentEnergy: getAddPermanentEnergy (state), - }) - -const mapDispatchToProps = (dispatch: ReduxDispatch): AttributesDispatchProps => ({ - addPoint: async (id: string) => { - await dispatch (AttributesActions.addAttributePoint (id)) - }, - removePoint: (id: string) => { - dispatch (AttributesActions.removeAttributePoint (id)) - }, - addLifePoint: async () => { - await dispatch (AttributesActions.addLifePoint) - }, - addArcaneEnergyPoint: async () => { - await dispatch (AttributesActions.addArcaneEnergyPoint) - }, - addKarmaPoint: async () => { - await dispatch (AttributesActions.addKarmaPoint) - }, - removeLifePoint: () => { - dispatch (AttributesActions.removeLifePoint ()) - }, - removeArcaneEnergyPoint: () => { - dispatch (AttributesActions.removeArcaneEnergyPoint ()) - }, - removeKarmaPoint: () => { - dispatch (AttributesActions.removeKarmaPoint ()) - }, - addLostLPPoint: () => { - dispatch (AttributesActions.addLostLPPoint ()) - }, - removeLostLPPoint: () => { - dispatch (AttributesActions.removeLostLPPoint ()) - }, - addLostLPPoints: (value: number) => { - dispatch (AttributesActions.addLostLPPoints (value)) - }, - addBoughtBackAEPoint: async () => { - await dispatch (AttributesActions.addBoughtBackAEPoint) - }, - removeBoughtBackAEPoint: () => { - dispatch (AttributesActions.removeBoughtBackAEPoint ()) - }, - addLostAEPoint: () => { - dispatch (AttributesActions.addLostAEPoint ()) - }, - removeLostAEPoint: () => { - dispatch (AttributesActions.removeLostAEPoint ()) - }, - addLostAEPoints: (value: number) => { - dispatch (AttributesActions.addLostAEPoints (value)) - }, - addBoughtBackKPPoint: async () => { - await dispatch (AttributesActions.addBoughtBackKPPoint) - }, - removeBoughtBackKPPoint: () => { - dispatch (AttributesActions.removeBoughtBackKPPoint ()) - }, - addLostKPPoint: () => { - dispatch (AttributesActions.addLostKPPoint ()) - }, - removeLostKPPoint: () => { - dispatch (AttributesActions.removeLostKPPoint ()) - }, - addLostKPPoints: (value: number) => { - dispatch (AttributesActions.addLostKPPoints (value)) - }, - openAddPermanentEnergyLoss: (energy: EnergyId) => { - console.log (`openAddPermanentEnergyLoss (${energy})`) - dispatch (SubwindowsActions.openAddPermanentEnergyLoss (energy)) - }, - closeAddPermanentEnergyLoss: () => { - console.log (`closeAddPermanentEnergyLoss ()`) - dispatch (SubwindowsActions.closeAddPermanentEnergyLoss ()) - }, - openEditPermanentEnergy: (energy: EnergyId) => { - console.log (`openEditPermanentEnergy (${energy})`) - dispatch (SubwindowsActions.openEditPermanentEnergy (energy)) - }, - closeEditPermanentEnergy: () => { - console.log (`closeEditPermanentEnergy ()`) - dispatch (SubwindowsActions.closeEditPermanentEnergy ()) - }, - setAdjustmentId: (id: Maybe) => { - if (isJust (id)) { - dispatch (AttributesActions.setAdjustmentId (fromJust (id))) - } - }, -}) - -const connectAttributes = - connect ( - mapStateToProps, - mapDispatchToProps - ) - -export const AttributesContainer = connectAttributes (Attributes) diff --git a/src/App/Containers/CombatTechniquesContainer.ts b/src/App/Containers/CombatTechniquesContainer.ts deleted file mode 100644 index fd18c3d9..00000000 --- a/src/App/Containers/CombatTechniquesContainer.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { connect } from "react-redux" -import { ReduxDispatch } from "../Actions/Actions" -import * as CombatTechniquesActions from "../Actions/CombatTechniquesActions" -import { AppStateRecord } from "../Models/AppState" -import { CombatTechniquesSortOptions } from "../Models/Config" -import { getAttributesForSheet } from "../Selectors/attributeSelectors" -import { getFilteredCombatTechniques } from "../Selectors/combatTechniquesSelectors" -import { getIsRemovingEnabled } from "../Selectors/phaseSelectors" -import { getCombatTechniquesFilterText } from "../Selectors/stateSelectors" -import { getCombatTechniquesSortOrder } from "../Selectors/uisettingsSelectors" -import { CombatTechniques, CombatTechniquesDispatchProps, CombatTechniquesOwnProps, CombatTechniquesStateProps } from "../Views/CombatTechniques/CombatTechniques" - -const mapStateToProps = ( - state: AppStateRecord, - ownProps: CombatTechniquesOwnProps -): CombatTechniquesStateProps => ({ - attributes: getAttributesForSheet (state, ownProps), - isRemovingEnabled: getIsRemovingEnabled (state), - list: getFilteredCombatTechniques (state, ownProps), - sortOrder: getCombatTechniquesSortOrder (state), - filterText: getCombatTechniquesFilterText (state), -}) - -const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ - async addPoint (id: string) { - await dispatch (CombatTechniquesActions.addCombatTechniquePoint (id)) - }, - removePoint (id: string) { - dispatch (CombatTechniquesActions.removeCombatTechniquePoint (id)) - }, - setSortOrder (sortOrder: CombatTechniquesSortOptions) { - dispatch (CombatTechniquesActions.setCombatTechniquesSortOrder (sortOrder)) - }, - setFilterText (filterText: string) { - dispatch (CombatTechniquesActions.setCombatTechniquesFilterText (filterText)) - }, -}) - -const connectCombatTechniques = - connect< - CombatTechniquesStateProps, - CombatTechniquesDispatchProps, - CombatTechniquesOwnProps, - AppStateRecord - > ( - mapStateToProps, - mapDispatchToProps - ) - -export const CombatTechniquesContainer = connectCombatTechniques (CombatTechniques) diff --git a/src/App/Containers/SettingsContainer.ts b/src/App/Containers/SettingsContainer.ts deleted file mode 100644 index c6721a9a..00000000 --- a/src/App/Containers/SettingsContainer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ipcRenderer } from "electron" -import { connect } from "react-redux" -import { fromJust, isJust, Maybe } from "../../Data/Maybe" -import { ReduxDispatch } from "../Actions/Actions" -import * as ConfigActions from "../Actions/ConfigActions" -import * as FallbackLocaleActions from "../Actions/FallbackLocaleActions" -import * as IOActions from "../Actions/IOActions" -import * as LocaleActions from "../Actions/LocaleActions" -import { AppStateRecord } from "../Models/AppState" -import { Locale, Theme } from "../Models/Config" -import { getUserSelectableSupportedLanguages } from "../Selectors/localeSelectors" -import { getFallbackLocaleId, getFallbackLocaleType, getLocaleId, getLocaleType } from "../Selectors/stateSelectors" -import { areAnimationsEnabled, getIsEditingHeroAfterCreationPhaseEnabled, getTheme } from "../Selectors/uisettingsSelectors" -import { IPCChannels } from "../Utilities/IPCChannels" -import { Settings, SettingsDispatchProps, SettingsOwnProps, SettingsStateProps } from "../Views/Settings/Settings" - -const mapStateToProps = (state: AppStateRecord): SettingsStateProps => ({ - localeString: getLocaleId (state), - localeType: getLocaleType (state), - fallbackLocaleString: getFallbackLocaleId (state), - fallbackLocaleType: getFallbackLocaleType (state), - isEditingHeroAfterCreationPhaseEnabled: getIsEditingHeroAfterCreationPhaseEnabled (state), - areAnimationsEnabled: areAnimationsEnabled (state), - theme: getTheme (state), - languages: getUserSelectableSupportedLanguages (state), - isCheckForUpdatesDisabled: !(ipcRenderer.sendSync (IPCChannels.IsUpdaterEnabled) as boolean), -}) - -const mapDispatchToProps = (dispatch: ReduxDispatch): SettingsDispatchProps => ({ - setTheme (theme: Maybe) { - if (isJust (theme)) { - dispatch (ConfigActions.setTheme (fromJust (theme))) - } - }, - switchEnableEditingHeroAfterCreationPhase () { - dispatch (ConfigActions.switchEnableEditingHeroAfterCreationPhase ()) - }, - async saveConfig () { - await dispatch (IOActions.requestConfigSave) - }, - setLocale (id: Maybe) { - dispatch (LocaleActions.setLocale (id)) - }, - setFallbackLocale (id: Maybe) { - dispatch (FallbackLocaleActions.setFallbackLocale (id)) - }, - switchEnableAnimations () { - dispatch (ConfigActions.switchEnableAnimations ()) - }, -}) - -const connectSettings = - connect ( - mapStateToProps, - mapDispatchToProps - ) - -export const SettingsContainer = connectSettings (Settings) diff --git a/src/App/Containers/SkillsContainer.ts b/src/App/Containers/SkillsContainer.ts deleted file mode 100644 index 908f8a14..00000000 --- a/src/App/Containers/SkillsContainer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { connect } from "react-redux" -import { ReduxDispatch } from "../Actions/Actions" -import * as SkillActions from "../Actions/SkillActions" -import { AppStateRecord } from "../Models/AppState" -import { SkillsSortOptions } from "../Models/Config" -import { getAttributesForSheet } from "../Selectors/attributeSelectors" -import { getIsRemovingEnabled } from "../Selectors/phaseSelectors" -import { getFilteredSkills, getSkillRating } from "../Selectors/skillsSelectors" -import { getSkillsFilterText } from "../Selectors/stateSelectors" -import { getSkillsCultureRatingVisibility, getSkillsSortOrder } from "../Selectors/uisettingsSelectors" -import { Skills, SkillsDispatchProps, SkillsOwnProps, SkillsStateProps } from "../Views/Skills/Skills" - -const mapStateToProps = (state: AppStateRecord, ownProps: SkillsOwnProps): SkillsStateProps => ({ - attributes: getAttributesForSheet (state, ownProps), - isRemovingEnabled: getIsRemovingEnabled (state), - list: getFilteredSkills (state), - sortOrder: getSkillsSortOrder (state), - filterText: getSkillsFilterText (state), - ratingVisibility: getSkillsCultureRatingVisibility (state), - skillRating: getSkillRating (state), -}) - -const mapDispatchToProps = (dispatch: ReduxDispatch): SkillsDispatchProps => ({ - async addPoint (id: string) { - await dispatch (SkillActions.addSkillPoint (id)) - }, - removePoint (id: string) { - dispatch (SkillActions.removeSkillPoint (id)) - }, - setSortOrder (sortOrder: SkillsSortOptions) { - dispatch (SkillActions.setSkillsSortOrder (sortOrder)) - }, - switchRatingVisibility () { - dispatch (SkillActions.switchSkillRatingVisibility ()) - }, - setFilterText (filterText: string) { - dispatch (SkillActions.setSkillsFilterText (filterText)) - }, -}) - -export const connectSkills = - connect ( - mapStateToProps, - mapDispatchToProps - ) - -export const SkillsContainer = connectSkills (Skills) diff --git a/src/App/Selectors/combatTechniquesSelectors.ts b/src/App/Selectors/combatTechniquesSelectors.ts deleted file mode 100644 index d8c49ef8..00000000 --- a/src/App/Selectors/combatTechniquesSelectors.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { ident, thrush } from "../../Data/Function" -import { fmap, fmapF } from "../../Data/Functor" -import { consF, filter, fnull, List, map } from "../../Data/List" -import { Just, liftM2, Maybe, maybe, Nothing } from "../../Data/Maybe" -import { add, divideBy, gt, max, subtractBy } from "../../Data/Num" -import { findWithDefault, foldrWithKey, lookup } from "../../Data/OrderedMap" -import { Record } from "../../Data/Record" -import { uncurryN } from "../../Data/Tuple/Curry" -import { IdPrefixes } from "../Constants/IdPrefixes" -import { CombatTechniqueId, SpecialAbilityId } from "../Constants/Ids" -import { ActivatableDependent } from "../Models/ActiveEntries/ActivatableDependent" -import { createSkillDependentWithValue6, SkillDependent } from "../Models/ActiveEntries/SkillDependent" -import { HeroModel, HeroModelRecord } from "../Models/Hero/HeroModel" -import { CombatTechniqueWithAttackParryBase, CombatTechniqueWithAttackParryBaseA_ } from "../Models/View/CombatTechniqueWithAttackParryBase" -import { CombatTechniqueWithRequirements } from "../Models/View/CombatTechniqueWithRequirements" -import { CombatTechnique } from "../Models/Wiki/CombatTechnique" -import { StaticData } from "../Models/Wiki/WikiModel" -import { isMaybeActive } from "../Utilities/Activatable/isActive" -import { createMaybeSelector } from "../Utilities/createMaybeSelector" -import { filterAndSortRecordsBy } from "../Utilities/filterAndSortBy" -import { compareLocale } from "../Utilities/I18n" -import { prefixId } from "../Utilities/IDUtils" -import { isDecreaseDisabled, isIncreaseDisabled } from "../Utilities/Increasable/combatTechniqueUtils" -import { pipe, pipe_ } from "../Utilities/pipe" -import { filterByAvailabilityAndPred, isEntryFromCoreBook } from "../Utilities/RulesUtils" -import { comparingR, sortByMulti } from "../Utilities/sortBy" -import { getMaxAttributeValueByID } from "./attributeSelectors" -import { getRuleBooksEnabled } from "./rulesSelectors" -import { getCombatTechniquesWithRequirementsSortOptions } from "./sortOptionsSelectors" -import { getAttributes, getCombatTechniques, getCombatTechniquesFilterText, getCurrentHeroPresent, getSpecialAbilities, getWiki, getWikiCombatTechniques } from "./stateSelectors" - -const CTA = CombatTechnique.A -const SDA = SkillDependent.A -const CTWAPBA = CombatTechniqueWithAttackParryBase.A -const CTWRA = CombatTechniqueWithRequirements.A -const ADA = ActivatableDependent.A - -/** - * Calculate the AT or PA mod by passing the current attributes' state as well - * as the relevant ids. - */ -const getPrimaryAttrMod = - (attributes: HeroModel["attributes"]) => - pipe ( - getMaxAttributeValueByID (attributes), - subtractBy (8), - divideBy (3), - Math.floor, - max (0) - ) - -const getAttackBase = - (attributes: HeroModel["attributes"]) => - (wiki_entry: Record) => - (hero_entry: Record): number => - pipe_ ( - CTA.gr (wiki_entry) === 2 - ? CTA.primary (wiki_entry) - : List (prefixId (IdPrefixes.ATTRIBUTES) (1)), - getPrimaryAttrMod (attributes), - add (SDA.value (hero_entry)) - ) - -const getParryBase = - (attributes: HeroModel["attributes"]) => - (wiki_entry: Record) => - (hero_entry: Record): Maybe => { - const curr_id = CTA.id (wiki_entry) - const curr_gr = CTA.gr (wiki_entry) - - return curr_gr === 2 - || curr_id === prefixId (IdPrefixes.COMBAT_TECHNIQUES) (6) - || curr_id === prefixId (IdPrefixes.COMBAT_TECHNIQUES) (8) - ? Nothing - : Just (Math.round (SDA.value (hero_entry) / 2) - + getPrimaryAttrMod (attributes) (CTA.primary (wiki_entry))) - } - -export const getCombatTechniquesForView = createMaybeSelector ( - getWiki, - getWikiCombatTechniques, - getAttributes, - getSpecialAbilities, - getCombatTechniques, - (staticData, wiki_combat_techniques, attributes, special_abilities, mcombatTechniques) => - fmapF (mcombatTechniques) - ((combatTechniques): List> => - pipe_ ( - wiki_combat_techniques, - foldrWithKey ((id: string) => (wiki_entry: Record) => { - const hero_entry = - findWithDefault (createSkillDependentWithValue6 (id)) - (id) - (combatTechniques) - - if (id === CombatTechniqueId.SpittingFire - && maybe (true) - (pipe (ADA.active, fnull)) - (lookup (SpecialAbilityId.Feuerschlucker) - (special_abilities))) { - // If SF Feuerschlucker is not active, do not - // show CT Spitting Fire - return ident as - ident>> - } - - return consF (CombatTechniqueWithAttackParryBase ({ - at: getAttackBase (attributes) (wiki_entry) (hero_entry), - pa: getParryBase (attributes) (wiki_entry) (hero_entry), - stateEntry: hero_entry, - wikiEntry: wiki_entry, - })) - }) - (List.empty), - sortByMulti ([ comparingR (CombatTechniqueWithAttackParryBaseA_.name) - (compareLocale (staticData)) ]) - )) -) - -export const getCombatTechniquesForSheet = createMaybeSelector ( - getWiki, - getCombatTechniquesForView, - (staticData, combatTechniques) => - fmapF (combatTechniques) - (filter (x => SDA.value (CTWAPBA.stateEntry (x)) > 6 - || isEntryFromCoreBook (CTA.src) - (StaticData.A.books (staticData)) - (CTWAPBA.wikiEntry (x)))) -) - -const getGr = pipe (CTWAPBA.wikiEntry, CTA.gr) -const getValue = pipe (CTWAPBA.stateEntry, SDA.value) -type CTWAPB = CombatTechniqueWithAttackParryBase - -export const getAllCombatTechniques = createMaybeSelector ( - getCombatTechniquesForView, - getCurrentHeroPresent, - getWiki, - (mcombat_techniques, mhero, wiki) => - liftM2 ((combatTechniques: List>) => (hero: HeroModelRecord) => { - const hunter = lookup (SpecialAbilityId.Hunter) - (HeroModel.A.specialAbilities (hero)) - - const hunterRequiresMinimum = - isMaybeActive (hunter) - && thrush (combatTechniques) (List.any (x => getGr (x) === 2 && getValue (x) >= 10)) - - return thrush (combatTechniques) - (map (x => - CombatTechniqueWithRequirements ({ - at: CTWAPBA.at (x), - pa: CTWAPBA.pa (x), - isDecreasable: !isDecreaseDisabled (wiki) - (hero) - (CTWAPBA.wikiEntry (x)) - (CTWAPBA.stateEntry (x)) - (hunterRequiresMinimum), - isIncreasable: !isIncreaseDisabled (wiki) - (hero) - (CTWAPBA.wikiEntry (x)) - (CTWAPBA.stateEntry (x)), - stateEntry: CTWAPBA.stateEntry (x), - wikiEntry: CTWAPBA.wikiEntry (x), - }))) - }) - (mcombat_techniques) - (mhero) -) - -export const getAvailableCombatTechniques = createMaybeSelector ( - getRuleBooksEnabled, - getAllCombatTechniques, - uncurryN (av => fmap (filterByAvailabilityAndPred (pipe (CTWRA.wikiEntry, CTA.src)) - (pipe (CTWRA.stateEntry, SDA.value, gt (6))) - (av))) -) - -export const getFilteredCombatTechniques = createMaybeSelector ( - getAvailableCombatTechniques, - getCombatTechniquesWithRequirementsSortOptions, - getCombatTechniquesFilterText, - (mcombat_techniques, sortOptions, filterText) => - fmapF (mcombat_techniques) - (filterAndSortRecordsBy (0) - ([ pipe (CTWRA.wikiEntry, CTA.name) ]) - (sortOptions) - (filterText)) -) diff --git a/src/App/Utilities/Increasable/attributeUtils.ts b/src/App/Utilities/Increasable/attributeUtils.ts deleted file mode 100644 index 45fa8ccb..00000000 --- a/src/App/Utilities/Increasable/attributeUtils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { fmap } from "../../../Data/Functor" -import { mapMaybe, maybe } from "../../../Data/Maybe" -import { lookupF, OrderedMap } from "../../../Data/OrderedMap" -import { Record } from "../../../Data/Record" -import { AttributeDependent } from "../../Models/ActiveEntries/AttributeDependent" -import { pipe } from "../pipe" - -const ADA = AttributeDependent.A - -export const getSkillCheckValues = - (attributes: OrderedMap>) => - mapMaybe (pipe (lookupF (attributes), fmap (ADA.value))) - -export const getAttributeValueWithDefault = maybe (8) (ADA.value) diff --git a/src/App/Utilities/Increasable/combatTechniqueUtils.ts b/src/App/Utilities/Increasable/combatTechniqueUtils.ts deleted file mode 100644 index 488fd3b4..00000000 --- a/src/App/Utilities/Increasable/combatTechniqueUtils.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { fmap } from "../../../Data/Functor" -import { cons, elem, foldl, List, maximum } from "../../../Data/List" -import { guard, Just, Maybe, maybe, then } from "../../../Data/Maybe" -import { add, divideBy, max, min } from "../../../Data/Num" -import { lookupF } from "../../../Data/OrderedMap" -import { Record } from "../../../Data/Record" -import { CombatTechniqueGroupId } from "../../Constants/Groups" -import { AdvantageId, AttrId } from "../../Constants/Ids" -import { AttributeDependent } from "../../Models/ActiveEntries/AttributeDependent" -import { SkillDependent } from "../../Models/ActiveEntries/SkillDependent" -import { HeroModel, HeroModelRecord } from "../../Models/Hero/HeroModel" -import { CombatTechnique } from "../../Models/Wiki/CombatTechnique" -import { ExperienceLevel } from "../../Models/Wiki/ExperienceLevel" -import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel" -import { getActiveSelectionsMaybe } from "../Activatable/selectionUtils" -import { flattenDependencies } from "../Dependencies/flattenDependencies" -import { pipe } from "../pipe" - -const ADA = AttributeDependent.A -const SkDA = SkillDependent.A -const CTA = CombatTechnique.A -const HA = HeroModel.A -const SDA = StaticData.A -const ELA = ExperienceLevel.A - -const getMaxPrimaryAttributeValueById = - (state: HeroModelRecord) => - foldl (currentMax => pipe ( - lookupF (HA.attributes (state)), - maybe (currentMax) (pipe (ADA.value, max (currentMax))) - )) - (8) - -const calculatePrimaryAttributeMod = pipe (add (-8), divideBy (3), Math.floor, max (0)) - -export const getPrimaryAttributeMod = - (state: HeroModelRecord) => - pipe (getMaxPrimaryAttributeValueById (state), calculatePrimaryAttributeMod) - -const getCombatTechniqueRating = maybe (6) (SkDA.value) - -export const getAttack = - (state: HeroModelRecord) => - (wikiEntry: Record) => - pipe ( - getCombatTechniqueRating, - add (getPrimaryAttributeMod (state) - (CTA.gr (wikiEntry) === CombatTechniqueGroupId.Ranged - ? CTA.primary (wikiEntry) - : List (AttrId.Courage))) - ) - -export const getParry = - (state: HeroModelRecord) => - (wikiEntry: Record) => - (maybeStateEntry: Maybe>): Maybe => - then (guard (CTA.gr (wikiEntry) !== CombatTechniqueGroupId.Ranged - && !CTA.hasNoParry (wikiEntry))) - (Just ( - Math.round (getCombatTechniqueRating (maybeStateEntry) / 2) - + getPrimaryAttributeMod (state) (CTA.primary (wikiEntry)) - )) - -export const isIncreaseDisabled = - (staticData: StaticDataRecord) => - (state: HeroModelRecord) => - (wikiEntry: Record) => - (instance: Record): boolean => { - const max_by_primary = getMaxPrimaryAttributeValueById (state) (CTA.primary (wikiEntry)) + 2 - - const mmax_by_el = then (guard (HA.phase (state) < 3)) - (fmap (ELA.maxCombatTechniqueRating) - (lookupF (SDA.experienceLevels (staticData)) - (HA.experienceLevel (state)))) - - const base_max = maybe (max_by_primary) (min (max_by_primary)) (mmax_by_el) - - const exceptionalSkill = lookupF (HA.advantages (state)) - (AdvantageId.ExceptionalCombatTechnique) - - const bonus = pipe ( - getActiveSelectionsMaybe, - fmap (elem (SkDA.id (instance))), - Maybe.elem (true), - x => x ? 1 : 0 - ) - (exceptionalSkill) - - return SkDA.value (instance) >= base_max + bonus - } - -export const isDecreaseDisabled = - (staticData: StaticDataRecord) => - (state: HeroModelRecord) => - (wikiEntry: Record) => - (instance: Record) => - (onlyOneCombatTechniqueForHunter: boolean): boolean => { - const disabledByHunter = - onlyOneCombatTechniqueForHunter - && CTA.gr (wikiEntry) === 2 - && SkDA.value (instance) === 10 - - return disabledByHunter - || SkDA.value (instance) <= maximum (cons (flattenDependencies (staticData) - (state) - (SkDA.dependencies (instance))) - (6)) - } diff --git a/src/App/Views/CombatTechniques/CombatTechniques.tsx b/src/App/Views/CombatTechniques/CombatTechniques.tsx deleted file mode 100644 index 1b3e13d7..00000000 --- a/src/App/Views/CombatTechniques/CombatTechniques.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from "react" -import { List, map, notNull, toArray } from "../../../Data/List" -import { bindF, ensure, Just, Maybe, maybe, Nothing } from "../../../Data/Maybe" -import { Record } from "../../../Data/Record" -import { WikiInfoContainer } from "../../Containers/WikiInfoContainer" -import { CombatTechniquesSortOptions } from "../../Models/Config" -import { HeroModelRecord } from "../../Models/Hero/HeroModel" -import { AttributeCombined } from "../../Models/View/AttributeCombined" -import { CombatTechniqueWithRequirements, CombatTechniqueWithRequirementsA_ } from "../../Models/View/CombatTechniqueWithRequirements" -import { StaticDataRecord } from "../../Models/Wiki/WikiModel" -import { translate } from "../../Utilities/I18n" -import { pipe, pipe_ } from "../../Utilities/pipe" -import { ListView } from "../Universal/List" -import { ListHeader } from "../Universal/ListHeader" -import { ListHeaderTag } from "../Universal/ListHeaderTag" -import { ListPlaceholder } from "../Universal/ListPlaceholder" -import { MainContent } from "../Universal/MainContent" -import { Options } from "../Universal/Options" -import { Page } from "../Universal/Page" -import { Scroll } from "../Universal/Scroll" -import { SearchField } from "../Universal/SearchField" -import { SortNames, SortOptions } from "../Universal/SortOptions" -import { CombatTechniqueListItem } from "./CombatTechniquesListItem" - -export interface CombatTechniquesOwnProps { - staticData: StaticDataRecord - hero: HeroModelRecord -} - -export interface CombatTechniquesStateProps { - attributes: List> - list: Maybe>> - isRemovingEnabled: boolean - sortOrder: CombatTechniquesSortOptions - filterText: string -} - -export interface CombatTechniquesDispatchProps { - setSortOrder (sortOrder: SortNames): void - addPoint (id: string): void - removePoint (id: string): void - setFilterText (filterText: string): void -} - -export type CombatTechniquesProps = - CombatTechniquesStateProps - & CombatTechniquesDispatchProps - & CombatTechniquesOwnProps - -export interface CombatTechniquesState { - infoId: Maybe -} - -const CTWRA_ = CombatTechniqueWithRequirementsA_ - -export const CombatTechniques: React.FC = props => { - const { - addPoint, - attributes, - list, - staticData, - isRemovingEnabled, - removePoint, - setSortOrder, - sortOrder, - filterText, - setFilterText, - } = props - - const [ infoId, setInfoId ] = React.useState> (Nothing) - - const showInfo = React.useCallback ( - (id: string) => setInfoId (Just (id)), - [ setInfoId ] - ) - - return ( - - - - - - - - - {translate (staticData) ("combattechniques.header.name")} - - - {translate (staticData) ("combattechniques.header.group")} - - - {translate (staticData) ("combattechniques.header.combattechniquerating")} - - - {translate (staticData) ("combattechniques.header.improvementcost")} - - - {translate (staticData) ("combattechniques.header.primaryattribute")} - - - {translate (staticData) ("combattechniques.header.attack")} - - - {translate (staticData) ("combattechniques.header.parry")} - - {isRemovingEnabled ? : null} - - - - - - {pipe_ ( - list, - bindF (ensure (notNull)), - maybe - () - (pipe ( - map ( - (x: Record) => ( - - ) - ), - toArray - )) - )} - - - - - - ) -} diff --git a/src/App/Views/CombatTechniques/CombatTechniquesListItem.tsx b/src/App/Views/CombatTechniques/CombatTechniquesListItem.tsx deleted file mode 100644 index 70035e84..00000000 --- a/src/App/Views/CombatTechniques/CombatTechniquesListItem.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from "react" -import { equals } from "../../../Data/Eq" -import { fmapF } from "../../../Data/Functor" -import { find, flength, intercalate, List } from "../../../Data/List" -import { fromMaybe, listToMaybe, mapMaybe, Maybe, maybe } from "../../../Data/Maybe" -import { lookupF } from "../../../Data/OrderedMap" -import { Record } from "../../../Data/Record" -import { NumIdName } from "../../Models/NumIdName" -import { AttributeCombined, AttributeCombinedA_ } from "../../Models/View/AttributeCombined" -import { CombatTechniqueWithRequirements, CombatTechniqueWithRequirementsA_ } from "../../Models/View/CombatTechniqueWithRequirements" -import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel" -import { ndash } from "../../Utilities/Chars" -import { pipe, pipe_ } from "../../Utilities/pipe" -import { SkillListItem } from "../Skills/SkillListItem" - -const ACA_ = AttributeCombinedA_ -const CTWRA = CombatTechniqueWithRequirements.A -const CTWRA_ = CombatTechniqueWithRequirementsA_ - -export interface CombatTechniqueListItemProps { - staticData: StaticDataRecord - attributes: List> - combatTechnique: Record - currentInfoId: Maybe - isRemovingEnabled: boolean - addPoint: (id: string) => void - removePoint: (id: string) => void - selectForInfo: (id: string) => void -} - -export const CombatTechniqueListItem: React.FC = props => { - const { - attributes, - currentInfoId, - isRemovingEnabled, - staticData, - combatTechnique: ct, - selectForInfo, - addPoint, - removePoint, - } = props - - const primary = - pipe_ ( - CTWRA_.primary (ct), - mapMaybe ((id: string) => fmapF (find (pipe (ACA_.id, equals (id))) - (attributes)) - (ACA_.short)), - intercalate ("/") - ) - - const customClassName = - flength (CTWRA_.primary (ct)) > 1 - ? "ATTR_6_8" - : fromMaybe ("") (listToMaybe (CTWRA_.primary (ct))) - - const primaryClassName = `primary ${customClassName}` - - return ( - (ndash) (CTWRA.pa (ct)), - } - )} - attributes={attributes} - staticData={staticData} - isRemovingEnabled={isRemovingEnabled} - selectForInfo={selectForInfo} - group={CTWRA_.gr (ct)} - getGroupName={ - pipe ( - lookupF (StaticData.A.combatTechniqueGroups (staticData)), - maybe (ndash) (NumIdName.A.name) - ) - } - selectedForInfo={currentInfoId} - /> - ) -} diff --git a/src/main_window/routes/Router.tsx b/src/main_window/routes/Router.tsx index e4cc782e..d1c1946c 100644 --- a/src/main_window/routes/Router.tsx +++ b/src/main_window/routes/Router.tsx @@ -7,6 +7,7 @@ import { LastChanges } from "./about/LastChanges.tsx" import { ThirdPartyLicenses } from "./about/ThirdPartyLicenses.tsx" import { Characters } from "./characters/Characters.tsx" import { Attributes } from "./characters/character/attributes/Attributes.tsx" +import { CombatTechniques } from "./characters/character/combatTechniques/CombatTechniques.tsx" import { ProfileOverview } from "./characters/character/profile/ProfileOverview.tsx" import { Rules } from "./characters/character/rules/Rules.tsx" import { Skills } from "./characters/character/skills/Skills.tsx" @@ -34,7 +35,7 @@ export const Router: FC = () => { case "disadvantages": return null case "skills": return - case "combat_techniques": return null + case "combat_techniques": return case "special_abilities": return null case "spells": return null case "liturgical_chants": return null diff --git a/src/main_window/routes/characters/character/combatTechniques/CloseCombatTechniquesListItem.tsx b/src/main_window/routes/characters/character/combatTechniques/CloseCombatTechniquesListItem.tsx new file mode 100644 index 00000000..9307721e --- /dev/null +++ b/src/main_window/routes/characters/character/combatTechniques/CloseCombatTechniquesListItem.tsx @@ -0,0 +1,135 @@ +// import * as React from "react" +// import { equals } from "../../../Data/Eq" +// import { fmapF } from "../../../Data/Functor" +// import { find, flength, intercalate, List } from "../../../Data/List" +// import { fromMaybe, listToMaybe, mapMaybe, Maybe, maybe } from "../../../Data/Maybe" +// import { lookupF } from "../../../Data/OrderedMap" +// import { Record } from "../../../Data/Record" +// import { NumIdName } from "../../Models/NumIdName" +// import { AttributeCombined, AttributeCombinedA_ } from "../../Models/View/AttributeCombined" +// import { CombatTechniqueWithRequirements, CombatTechniqueWithRequirementsA_ } from "../../Models/View/CombatTechniqueWithRequirements" +// import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel" +// import { ndash } from "../../Utilities/Chars" +// import { pipe, pipe_ } from "../../Utilities/pipe" +// import { SkillListItem } from "../Skills/SkillListItem" + +import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences" +import { FC, memo, useCallback } from "react" +import { ListItem } from "../../../../../shared/components/list/ListItem.tsx" +import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx" +import { ListItemName } from "../../../../../shared/components/list/ListItemName.tsx" +import { ListItemSeparator } from "../../../../../shared/components/list/ListItemSeparator.tsx" +import { ListItemValues } from "../../../../../shared/components/list/ListItemValues.tsx" +import { ImprovementCost } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" +import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" +import { selectAttributes } from "../../../../slices/databaseSlice.ts" +import { + changeInlineLibraryEntry, + selectInlineLibraryEntryId, +} from "../../../../slices/inlineWikiSlice.ts" +import { SkillAdditionalValues } from "../skills/SkillAdditionalValues.tsx" +import { SkillButtons } from "../skills/SkillButtons.tsx" +import { SkillImprovementCost } from "../skills/SkillImprovementCost.tsx" +import { SkillRating } from "../skills/SkillRating.tsx" + +type Props = { + insertTopMargin?: boolean + id: number + name: string + sr: number + primary: AttributeReference[] + ic: ImprovementCost + addDisabled: boolean + removeDisabled: boolean + at: number + pa?: number + addPoint: (id: number) => void + removePoint: (id: number) => void +} + +const CloseCombatTechniquesListItem: FC = props => { + const { + insertTopMargin, + id, + name, + sr, + primary, + ic, + addDisabled, + removeDisabled, + at, + pa, + addPoint, + removePoint, + } = props + + const translate = useTranslate() + const translateMap = useTranslateMap() + const dispatch = useAppDispatch() + const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId) + const canRemove = useAppSelector(selectCanRemove) + + const handleSelectForInfo = useCallback( + () => + dispatch( + changeInlineLibraryEntry({ tag: "CloseCombatTechnique", close_combat_technique: id }), + ), + [dispatch, id], + ) + + const attributes = useAppSelector(selectAttributes) + + const primaryStr = primary + .map(ref => translateMap(attributes[ref.id.attribute]?.translations)?.abbreviation ?? "") + .join("/") + + const customClassName = `attr--${primary.map(ref => ref.id.attribute).join("-")}` + + const primaryClassName = `primary ${customClassName}` + + return ( + + + + + + + + + + + + ) +} + +const MemoCloseCombatTechniquesListItem = memo(CloseCombatTechniquesListItem) + +export { MemoCloseCombatTechniquesListItem as CloseCombatTechniquesListItem } diff --git a/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx b/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx new file mode 100644 index 00000000..208a0e33 --- /dev/null +++ b/src/main_window/routes/characters/character/combatTechniques/CombatTechniques.tsx @@ -0,0 +1,256 @@ +// import * as React from "react" +// import { List, map, notNull, toArray } from "../../../../../Data/List.ts" +// import { bindF, ensure, Just, Maybe, maybe, Nothing } from "../../../../../Data/Maybe.ts" +// import { Record } from "../../../../../Data/Record.ts" +// import { WikiInfoContainer } from "../../../../../App/Containers/WikiInfoContainer.ts" +// import { CombatTechniquesSortOptions } from "../../../../../App/Models/Config.ts" +// import { HeroModelRecord } from "../../../../../App/Models/Hero/HeroModel.ts" +// import { AttributeCombined } from "../../../../../App/Models/View/AttributeCombined.ts" +// import { CombatTechniqueWithRequirements, CombatTechniqueWithRequirementsA_ } from "../../../../../App/Models/View/CombatTechniqueWithRequirements.ts" +// import { StaticDataRecord } from "../../../../../App/Models/Wiki/WikiModel.ts" +// import { translate } from "../../../../../App/Utilities/I18n.ts" +// import { pipe, pipe_ } from "../../../../../App/Utilities/pipe.ts" +// import { ListView } from "../Universal/List" +// import { ListHeader } from "../Universal/ListHeader" +// import { ListHeaderTag } from "../Universal/ListHeaderTag" +// import { ListPlaceholder } from "../Universal/ListPlaceholder" +// import { MainContent } from "../Universal/MainContent" +// import { Options } from "../Universal/Options" +// import { Page } from "../Universal/Page" +// import { Scroll } from "../Universal/Scroll" +// import { SearchField } from "../../../../../App/Views/Universal/SearchField.tsx" +// import { SortNames, SortOptions } from "../../../../../App/Views/Universal/SortOptions.tsx" +// import { CombatTechniqueListItem } from "./CombatTechniquesListItem" + +import { FC, useCallback, useMemo, useState } from "react" +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" +import { Main } from "../../../../../shared/components/main/Main.tsx" +import { Options } from "../../../../../shared/components/options/Options.tsx" +import { Page } from "../../../../../shared/components/page/Page.tsx" +import { RadioButtonGroup } from "../../../../../shared/components/radioButton/RadioButtonGroup.tsx" +import { Scroll } from "../../../../../shared/components/scroll/Scroll.tsx" +import { TextField } from "../../../../../shared/components/textField/TextField.tsx" +import { + compareImprovementCost, + fromRaw, +} from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts" +import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { compareAt, numAsc, reduceCompare } from "../../../../../shared/utils/compare.ts" +import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts" +import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" +import { InlineLibrary } from "../../../../inlineLibrary/InlineLibrary.tsx" +import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" +import { + DisplayedCombatTechnique, + selectVisibleCombatTechniques, +} from "../../../../selectors/combatTechniquesSelectors.ts" +import { + decrementCloseCombatTechnique, + incrementCloseCombatTechnique, +} from "../../../../slices/closeCombatTechniqueSlice.ts" +import { + decrementRangedCombatTechnique, + incrementRangedCombatTechnique, +} from "../../../../slices/rangedCombatTechniqueSlice.ts" +import { + CombatTechniquesSortOrder, + changeCombatTechniquesSortOrder, + selectCombatTechniquesSortOrder, +} from "../../../../slices/settingsSlice.ts" +import { CloseCombatTechniquesListItem } from "./CloseCombatTechniquesListItem.tsx" +import { RangedCombatTechniquesListItem } from "./RangedCombatTechniquesListItem.tsx" + +const isTopMarginNeeded = ( + sortOrder: CombatTechniquesSortOrder, + curr: DisplayedCombatTechnique, + mprev: DisplayedCombatTechnique | undefined, +) => sortOrder === "group" && mprev !== undefined && curr.kind !== mprev.kind + +export const CombatTechniques: FC = () => { + const translate = useTranslate() + const translateMap = useTranslateMap() + const dispatch = useAppDispatch() + const localeCompare = useLocaleCompare() + + const canRemove = useAppSelector(selectCanRemove) + + const [filterText, setFilterText] = useState("") + const sortOrder = useAppSelector(selectCombatTechniquesSortOrder) + const handleChangeSortOrder = useCallback( + (id: CombatTechniquesSortOrder) => dispatch(changeCombatTechniquesSortOrder(id)), + [dispatch], + ) + const visibleCombatTechniques = useAppSelector(selectVisibleCombatTechniques) + const list = useMemo( + () => + visibleCombatTechniques + .filter( + c => + translateMap(c.static.translations) + ?.name.toLowerCase() + .includes(filterText.toLowerCase()) ?? false, + ) + .sort( + (() => { + switch (sortOrder) { + case CombatTechniquesSortOrder.Name: + return compareAt( + c => translateMap(c.static.translations)?.name ?? "", + localeCompare, + ) + case CombatTechniquesSortOrder.Group: + return reduceCompare( + compareAt(c => (c.kind === "close" ? 1 : 2), numAsc), + compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare), + ) + case CombatTechniquesSortOrder.ImprovementCost: + return reduceCompare( + compareAt(c => fromRaw(c.static.improvement_cost), compareImprovementCost), + compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare), + ) + default: + return assertExhaustive(sortOrder) + } + })(), + ), + [filterText, localeCompare, sortOrder, translateMap, visibleCombatTechniques], + ) + + const handleAddCloseCombatTechniquePoint = useCallback( + (id: number) => dispatch(incrementCloseCombatTechnique(id)), + [dispatch], + ) + + const handleRemoveCloseCombatTechniquePoint = useCallback( + (id: number) => dispatch(decrementCloseCombatTechnique(id)), + [dispatch], + ) + + const handleAddRangedCombatTechniquePoint = useCallback( + (id: number) => dispatch(incrementRangedCombatTechnique(id)), + [dispatch], + ) + + const handleRemoveRangedCombatTechniquePoint = useCallback( + (id: number) => dispatch(decrementRangedCombatTechnique(id)), + [dispatch], + ) + + return ( + + + + + +
+ + + {translate("combattechniques.header.name")} + + + {translate("combattechniques.header.group")} + + + {translate("combattechniques.header.combattechniquerating")} + + + {translate("combattechniques.header.improvementcost")} + + + {translate("combattechniques.header.primaryattribute")} + + + {translate("combattechniques.header.attack")} + + + {translate("combattechniques.header.parry")} + + {canRemove ? : null} + + + + + {list.length > 0 ? ( + + {list.map((x, i) => { + const translation = translateMap(x.static.translations) + + if (x.kind === "close") { + return ( + + ) + } + + return ( + + ) + })} + + ) : ( + + )} + +
+ +
+ ) +} diff --git a/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx b/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx new file mode 100644 index 00000000..90871803 --- /dev/null +++ b/src/main_window/routes/characters/character/combatTechniques/RangedCombatTechniquesListItem.tsx @@ -0,0 +1,128 @@ +// import * as React from "react" +// import { equals } from "../../../Data/Eq" +// import { fmapF } from "../../../Data/Functor" +// import { find, flength, intercalate, List } from "../../../Data/List" +// import { fromMaybe, listToMaybe, mapMaybe, Maybe, maybe } from "../../../Data/Maybe" +// import { lookupF } from "../../../Data/OrderedMap" +// import { Record } from "../../../Data/Record" +// import { NumIdName } from "../../Models/NumIdName" +// import { AttributeCombined, AttributeCombinedA_ } from "../../Models/View/AttributeCombined" +// import { CombatTechniqueWithRequirements, CombatTechniqueWithRequirementsA_ } from "../../Models/View/CombatTechniqueWithRequirements" +// import { StaticData, StaticDataRecord } from "../../Models/Wiki/WikiModel" +// import { ndash } from "../../Utilities/Chars" +// import { pipe, pipe_ } from "../../Utilities/pipe" +// import { SkillListItem } from "../Skills/SkillListItem" + +import { AttributeReference } from "optolith-database-schema/types/_SimpleReferences" +import { FC, memo, useCallback } from "react" +import { ListItem } from "../../../../../shared/components/list/ListItem.tsx" +import { ListItemGroup } from "../../../../../shared/components/list/ListItemGroup.tsx" +import { ListItemName } from "../../../../../shared/components/list/ListItemName.tsx" +import { ListItemSeparator } from "../../../../../shared/components/list/ListItemSeparator.tsx" +import { ListItemValues } from "../../../../../shared/components/list/ListItemValues.tsx" +import { ImprovementCost } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { useTranslate } from "../../../../../shared/hooks/translate.ts" +import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" +import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" +import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" +import { selectAttributes } from "../../../../slices/databaseSlice.ts" +import { changeInlineLibraryEntry, selectInlineLibraryEntryId } from "../../../../slices/inlineWikiSlice.ts" +import { SkillAdditionalValues } from "../skills/SkillAdditionalValues.tsx" +import { SkillButtons } from "../skills/SkillButtons.tsx" +import { SkillImprovementCost } from "../skills/SkillImprovementCost.tsx" +import { SkillRating } from "../skills/SkillRating.tsx" + +type Props = { + insertTopMargin?: boolean + id: number + name: string + sr: number + primary: AttributeReference[] + ic: ImprovementCost + addDisabled: boolean + removeDisabled: boolean + at: number + addPoint: (id: number) => void + removePoint: (id: number) => void +} + +const RangedCombatTechniquesListItem: FC = props => { + const { + insertTopMargin, + id, + name, + sr, + primary, + ic, + addDisabled, + removeDisabled, + at, + addPoint, + removePoint, + } = props + + const translate = useTranslate() + const translateMap = useTranslateMap() + const dispatch = useAppDispatch() + const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId) + const canRemove = useAppSelector(selectCanRemove) + + const handleSelectForInfo = + useCallback( + () => dispatch(changeInlineLibraryEntry({ tag: "RangedCombatTechnique", ranged_combat_technique: id })), + [ dispatch, id ] + ) + + const attributes = useAppSelector(selectAttributes) + + const primaryStr = primary + .map(ref => translateMap(attributes[ref.id.attribute]?.translations)?.abbreviation ?? "") + .join("/") + + const customClassName = `attr--${primary.map(ref => ref.id.attribute).join("-")}` + + const primaryClassName = `primary ${customClassName}` + + return ( + + + + + + + + + + + + ) +} + +const MemoRangedCombatTechniquesListItem = memo(RangedCombatTechniquesListItem) + +export { MemoRangedCombatTechniquesListItem as RangedCombatTechniquesListItem } diff --git a/src/main_window/routes/characters/character/skills/SkillListItem.tsx b/src/main_window/routes/characters/character/skills/SkillListItem.tsx index 50c103d0..cd52cb1f 100644 --- a/src/main_window/routes/characters/character/skills/SkillListItem.tsx +++ b/src/main_window/routes/characters/character/skills/SkillListItem.tsx @@ -1,4 +1,7 @@ -import { SkillCheckPenalty, SkillCheck as SkillCheckType } from "optolith-database-schema/types/_SkillCheck" +import { + SkillCheckPenalty, + SkillCheck as SkillCheckType, +} from "optolith-database-schema/types/_SkillCheck" import { memo, useCallback } from "react" import { ListItem } from "../../../../../shared/components/list/ListItem.tsx" import { ListItemName } from "../../../../../shared/components/list/ListItemName.tsx" @@ -7,7 +10,10 @@ import { ListItemValues } from "../../../../../shared/components/list/ListItemVa import { ImprovementCost } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" -import { changeInlineLibraryEntry, selectInlineLibraryEntryId } from "../../../../slices/inlineWikiSlice.ts" +import { + changeInlineLibraryEntry, + selectInlineLibraryEntryId, +} from "../../../../slices/inlineWikiSlice.ts" import { AdditionalValue, SkillAdditionalValues } from "./SkillAdditionalValues.tsx" import { SkillButtons } from "./SkillButtons.tsx" import { SkillCheck } from "./SkillCheck.tsx" @@ -73,11 +79,10 @@ const SkillListItem: React.FC = props => { const inlineLibraryEntryId = useAppSelector(selectInlineLibraryEntryId) const canRemove = useAppSelector(selectCanRemove) - const handleSelectForInfo = - useCallback( - () => dispatch(changeInlineLibraryEntry({ tag: "Skill", skill: id })), - [ dispatch, id ] - ) + const handleSelectForInfo = useCallback( + () => dispatch(changeInlineLibraryEntry({ tag: "Skill", skill: id })), + [dispatch, id], + ) return ( = props => { unrecommended={untyp} insertTopMargin={insertTopMargin} active={inlineLibraryEntryId?.tag === "Skill" && inlineLibraryEntryId.skill === id} - > + > - + - {checkDisabled !== true && check !== undefined - ? ( - - ) - : null} - - - + /> + {checkDisabled !== true && check !== undefined ? ( + + ) : null} + + + = props => { addPoint={addPoint} removePoint={canRemove ? removePoint : undefined} selectForInfo={handleSelectForInfo} - /> + /> ) } diff --git a/src/main_window/routes/characters/character/skills/Skills.tsx b/src/main_window/routes/characters/character/skills/Skills.tsx index ef0b9012..ff445e49 100644 --- a/src/main_window/routes/characters/character/skills/Skills.tsx +++ b/src/main_window/routes/characters/character/skills/Skills.tsx @@ -12,7 +12,10 @@ import { RadioButtonGroup } from "../../../../../shared/components/radioButton/R import { RecommendedReference } from "../../../../../shared/components/recommendedReference/RecommendedReference.tsx" import { Scroll } from "../../../../../shared/components/scroll/Scroll.tsx" import { TextField } from "../../../../../shared/components/textField/TextField.tsx" -import { compareImprovementCost, fromRaw } from "../../../../../shared/domain/adventurePoints/improvementCost.ts" +import { + compareImprovementCost, + fromRaw, +} from "../../../../../shared/domain/adventurePoints/improvementCost.ts" import { useLocaleCompare } from "../../../../../shared/hooks/localeCompare.ts" import { useTranslate } from "../../../../../shared/hooks/translate.ts" import { useTranslateMap } from "../../../../../shared/hooks/translateMap.ts" @@ -23,16 +26,25 @@ import { InlineLibrary } from "../../../../inlineLibrary/InlineLibrary.tsx" import { selectCanRemove } from "../../../../selectors/characterSelectors.ts" import { DisplayedSkill, selectVisibleSkills } from "../../../../selectors/skillsSelectors.ts" import { selectSkillGroups } from "../../../../slices/databaseSlice.ts" -import { SkillsSortOrder, changeSkillsSortOrder, selectSkillsCultureRatingVisibility, selectSkillsSortOrder, switchSkillsCultureRatingVisibility } from "../../../../slices/settingsSlice.ts" +import { + SkillsSortOrder, + changeSkillsSortOrder, + selectSkillsCultureRatingVisibility, + selectSkillsSortOrder, + switchSkillsCultureRatingVisibility, +} from "../../../../slices/settingsSlice.ts" import { decrementSkill, incrementSkill } from "../../../../slices/skillsSlice.ts" import { SkillListItem } from "./SkillListItem.tsx" import "./Skills.scss" -const isTopMarginNeeded = - (sortOrder: SkillsSortOrder, curr: DisplayedSkill, mprev: DisplayedSkill | undefined) => - sortOrder === "group" - && mprev !== undefined - && curr.static.group.id.skill_group !== mprev.static.group.id.skill_group +const isTopMarginNeeded = ( + sortOrder: SkillsSortOrder, + curr: DisplayedSkill, + mprev: DisplayedSkill | undefined, +) => + sortOrder === "group" && + mprev !== undefined && + curr.static.group.id.skill_group !== mprev.static.group.id.skill_group export const Skills: FC = () => { const translate = useTranslate() @@ -43,67 +55,67 @@ export const Skills: FC = () => { const cultureRatingVisibility = useAppSelector(selectSkillsCultureRatingVisibility) const handlSwitchCultureRatingVisibility = useCallback( () => dispatch(switchSkillsCultureRatingVisibility()), - [ dispatch ] + [dispatch], ) const canRemove = useAppSelector(selectCanRemove) const skillGroups = useAppSelector(selectSkillGroups) - const [ filterText, setFilterText ] = useState("") + const [filterText, setFilterText] = useState("") const sortOrder = useAppSelector(selectSkillsSortOrder) const handleChangeSortOrder = useCallback( (id: SkillsSortOrder) => dispatch(changeSkillsSortOrder(id)), - [ dispatch ] + [dispatch], ) const visibleSkills = useAppSelector(selectVisibleSkills) const list = useMemo( - () => visibleSkills - .filter(c => translateMap(c.static.translations)?.name.toLowerCase() - .includes(filterText.toLowerCase()) ?? false) - .sort((() => { - switch (sortOrder) { - case SkillsSortOrder.Name: - return compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare) - case SkillsSortOrder.Group: - return reduceCompare( - compareAt(c => c.static.group.id.skill_group, numAsc), - compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare), - ) - case SkillsSortOrder.ImprovementCost: - return reduceCompare( - compareAt(c => fromRaw(c.static.improvement_cost), compareImprovementCost), - compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare), - ) - default: - return assertExhaustive(sortOrder) - } - })()), - [ filterText, localeCompare, sortOrder, translateMap, visibleSkills ] + () => + visibleSkills + .filter( + c => + translateMap(c.static.translations) + ?.name.toLowerCase() + .includes(filterText.toLowerCase()) ?? false, + ) + .sort( + (() => { + switch (sortOrder) { + case SkillsSortOrder.Name: + return compareAt( + c => translateMap(c.static.translations)?.name ?? "", + localeCompare, + ) + case SkillsSortOrder.Group: + return reduceCompare( + compareAt(c => c.static.group.id.skill_group, numAsc), + compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare), + ) + case SkillsSortOrder.ImprovementCost: + return reduceCompare( + compareAt(c => fromRaw(c.static.improvement_cost), compareImprovementCost), + compareAt(c => translateMap(c.static.translations)?.name ?? "", localeCompare), + ) + default: + return assertExhaustive(sortOrder) + } + })(), + ), + [filterText, localeCompare, sortOrder, translateMap, visibleSkills], ) - const handleAdd = useCallback( - (id: number) => dispatch(incrementSkill(id)), - [ dispatch ] - ) + const handleAdd = useCallback((id: number) => dispatch(incrementSkill(id)), [dispatch]) - const handleRemove = useCallback( - (id: number) => dispatch(decrementSkill(id)), - [ dispatch ] - ) + const handleRemove = useCallback((id: number) => dispatch(decrementSkill(id)), [dispatch]) const getGroupName = useCallback( (id: number) => translateMap(skillGroups[id]?.translations)?.name ?? "", - [ skillGroups, translateMap ] + [skillGroups, translateMap], ) return ( - + { }, ]} onClick={handleChangeSortOrder} - /> + /> - + {translate("skills.commonskills")} {cultureRatingVisibility ? : null} @@ -135,25 +144,13 @@ export const Skills: FC = () => {
- - {translate("skills.header.name")} - - - {translate("skills.header.group")} - - + {translate("skills.header.name")} + {translate("skills.header.group")} + {translate("skills.header.skillrating")} - - {translate("skills.header.check")} - - + {translate("skills.header.check")} + {translate("skills.header.improvementcost")} {canRemove ? : null} @@ -161,37 +158,35 @@ export const Skills: FC = () => { - { - list.length > 0 - ? ( - - {list.map((x, i) => { - const translation = translateMap(x.static.translations) - return ( - - ) - })} - - ) - : - } + {list.length > 0 ? ( + + {list.map((x, i) => { + const translation = translateMap(x.static.translations) + return ( + + ) + })} + + ) : ( + + )}
diff --git a/src/main_window/selectors/adventurePointSelectors.ts b/src/main_window/selectors/adventurePointSelectors.ts index b43fd03c..69c0f8f1 100644 --- a/src/main_window/selectors/adventurePointSelectors.ts +++ b/src/main_window/selectors/adventurePointSelectors.ts @@ -1,6 +1,16 @@ import { createSelector } from "@reduxjs/toolkit" -import { ImprovementCost, adventurePointsForRange } from "../../shared/domain/adventurePoints/improvementCost.ts" -import { RatedMap, selectAttributes, selectCurrentCharacter, selectDerivedCharacteristics, selectSkills, selectTotalAdventurePoints } from "../slices/characterSlice.ts" +import { + ImprovementCost, + adventurePointsForRange, +} from "../../shared/domain/adventurePoints/improvementCost.ts" +import { RatedMap } from "../../shared/domain/ratedEntry.ts" +import { + selectAttributes, + selectCurrentCharacter, + selectDerivedCharacteristics, + selectSkills, + selectTotalAdventurePoints, +} from "../slices/characterSlice.ts" export type SpentAdventurePoints = { general: number @@ -13,72 +23,66 @@ const sumRatedMap = (ratedMap: RatedMap): SpentAdventurePoints => general: acc.general + rated.cachedAdventurePoints.general, bound: acc.bound + rated.cachedAdventurePoints.bound, }), - { general: 0, bound: 0 } + { general: 0, bound: 0 }, ) -export const selectAdventurePointsSpentOnAttributes = createSelector( - selectAttributes, - sumRatedMap -) +export const selectAdventurePointsSpentOnAttributes = createSelector(selectAttributes, sumRatedMap) -export const selectAdventurePointsSpentOnSkills = createSelector( - selectSkills, - sumRatedMap -) +export const selectAdventurePointsSpentOnSkills = createSelector(selectSkills, sumRatedMap) export const selectAdventurePointsSpentOnCombatTechniques = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnSpells = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnLiturgicalChants = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnCantrips = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnBlessings = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnAdvantages = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnMagicalAdvantages = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnBlessedAdvantages = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnDisadvantages = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnMagicalDisadvantages = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnBlessedDisadvantages = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) // export const getMagicalAdvantagesDisadvantagesAdventurePointsMaximum = createMaybeSelector( @@ -88,7 +92,7 @@ export const selectAdventurePointsSpentOnBlessedDisadvantages = createSelector( export const selectAdventurePointsSpentOnSpecialAbilities = createSelector( selectCurrentCharacter, - (): SpentAdventurePoints => ({ general: 0, bound: 0 }) + (): SpentAdventurePoints => ({ general: 0, bound: 0 }), ) export const selectAdventurePointsSpentOnEnergies = createSelector( @@ -100,20 +104,20 @@ export const selectAdventurePointsSpentOnEnergies = createSelector( derivedCharacteristics.karmaPoints.purchased, ].reduce( (acc, purchased) => acc + adventurePointsForRange(ImprovementCost.D, 0, purchased), - 0 - ) - + derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack * 2 - + derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack * 2 + 0, + ) + + derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack * 2 + + derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack * 2, ) export const selectAdventurePointsSpentOnRace = createSelector( selectCurrentCharacter, - (): number => 0 + (): number => 0, ) export const selectAdventurePointsSpentOnProfession = createSelector( selectCurrentCharacter, - (): number | undefined => undefined + (): number | undefined => undefined, ) export const selectAdventurePointsSpent = createSelector( @@ -134,34 +138,33 @@ export const selectAdventurePointsSpent = createSelector( selectAdventurePointsSpentOnEnergies, selectAdventurePointsSpentOnRace, selectAdventurePointsSpentOnProfession, - (...spentCategories): SpentAdventurePoints => spentCategories.reduce( - (acc, spentCategory) => ({ - general: - acc.general - + ( - typeof spentCategory === "number" - ? spentCategory - : typeof spentCategory === "object" - ? spentCategory.general - : 0), - bound: - acc.general - + ( - typeof spentCategory === "number" - ? 0 - : typeof spentCategory === "object" - ? spentCategory.bound - : 0), - }), - { general: 0, bound: 0 } - ) + (...spentCategories): SpentAdventurePoints => + spentCategories.reduce( + (acc, spentCategory) => ({ + general: + acc.general + + (typeof spentCategory === "number" + ? spentCategory + : typeof spentCategory === "object" + ? spentCategory.general + : 0), + bound: + acc.general + + (typeof spentCategory === "number" + ? 0 + : typeof spentCategory === "object" + ? spentCategory.bound + : 0), + }), + { general: 0, bound: 0 }, + ), ) export const selectAdventurePointsAvailable = createSelector( selectTotalAdventurePoints, selectAdventurePointsSpent, (totalAdventurePoints = 0, { general: spentAdventurePoints }) => - totalAdventurePoints - spentAdventurePoints + totalAdventurePoints - spentAdventurePoints, ) // export const getHasCurrentNoAddedAP = createMaybeSelector ( diff --git a/src/main_window/selectors/attributeSelectors.ts b/src/main_window/selectors/attributeSelectors.ts index f7c41867..633a1c91 100644 --- a/src/main_window/selectors/attributeSelectors.ts +++ b/src/main_window/selectors/attributeSelectors.ts @@ -1,18 +1,140 @@ import { createSelector } from "@reduxjs/toolkit" import { Attribute } from "optolith-database-schema/types/Attribute" -import { getAttributeMaximum, getAttributeMinimum, isAttributeDecreasable, isAttributeIncreasable } from "../../shared/domain/attribute.ts" +import { + getAttributeMaximum, + getAttributeMinimum, + isAttributeDecreasable, + isAttributeIncreasable, +} from "../../shared/domain/attribute.ts" +import { filterApplyingRatedDependencies } from "../../shared/domain/dependencies/filterApplyingDependencies.ts" import { OptionalRuleIdentifier } from "../../shared/domain/identifier.ts" import { Rated } from "../../shared/domain/ratedEntry.ts" import { isNotNullish } from "../../shared/utils/nullable.ts" import { createPropertySelector } from "../../shared/utils/redux.ts" import { attributeValue, createInitialDynamicAttribute } from "../slices/attributesSlice.ts" -import { selectActiveOptionalRules, selectAttributeAdjustmentId, selectDerivedCharacteristics, selectAttributes as selectDynamicAttributes } from "../slices/characterSlice.ts" +import { + selectActiveOptionalRules, + selectAttributeAdjustmentId, + selectCeremonies, + selectCloseCombatTechniques, + selectDerivedCharacteristics, + selectAttributes as selectDynamicAttributes, + selectLiturgicalChants, + selectRangedCombatTechniques, + selectRituals, + selectSkills, + selectSpells, +} from "../slices/characterSlice.ts" import { selectAttributes as selectStaticAttributes } from "../slices/databaseSlice.ts" import { selectIsInCharacterCreation } from "./characterSelectors.ts" -import { selectCurrentExperienceLevel, selectMaximumTotalAttributePoints, selectStartExperienceLevel } from "./experienceLevelSelectors.ts" -import { selectActiveBlessedTradition, selectActiveMagicalTraditions } from "./magicalTraditionSelectors.ts" +import { + selectCurrentExperienceLevel, + selectMaximumTotalAttributePoints, + selectStartExperienceLevel, +} from "./experienceLevelSelectors.ts" +import { + selectActiveBlessedTradition, + selectActiveMagicalTraditions, +} from "./magicalTraditionSelectors.ts" import { selectCurrentRace } from "./raceSelectors.ts" +export type DisplayedPrimaryAttribute = { + static: Attribute + dynamic: Rated +} + +export type DisplayedMagicalPrimaryAttributes = { + list: DisplayedPrimaryAttribute[] + halfed: boolean +} + +export const selectHighestMagicalPrimaryAttributes = createSelector( + selectActiveMagicalTraditions, + selectStaticAttributes, + selectDynamicAttributes, + ( + activeMagicalTraditions, + staticAttributes, + dynamicAttributes, + ): DisplayedMagicalPrimaryAttributes => { + const { map, halfed } = activeMagicalTraditions.reduce<{ + map: Map + halfed: boolean + }>( + (currentlyHighest, magicalTradition) => { + const staticPrimaryAttribute = magicalTradition.static.primary + + if (staticPrimaryAttribute === undefined) { + return currentlyHighest + } else { + const { + id: { attribute: id }, + use_half_for_arcane_energy, + } = staticPrimaryAttribute + const staticAttribute = staticAttributes[id] + const dynamicAttribute = dynamicAttributes[id] ?? createInitialDynamicAttribute(id) + + if (staticAttribute === undefined) { + return currentlyHighest + } else if ( + currentlyHighest.map.size === 0 || + [...currentlyHighest.map.values()][0]!.dynamic.value < dynamicAttribute.value + ) { + return { + map: new Map([[id, { static: staticAttribute, dynamic: dynamicAttribute }]]), + halfed: currentlyHighest.halfed || use_half_for_arcane_energy, + } + } else if ( + [...currentlyHighest.map.values()][0]!.dynamic.value === dynamicAttribute.value + ) { + return { + map: currentlyHighest.map.set(id, { + static: staticAttribute, + dynamic: dynamicAttribute, + }), + halfed: currentlyHighest.halfed || use_half_for_arcane_energy, + } + } else { + return currentlyHighest + } + } + }, + { map: new Map(), halfed: false }, + ) + + return { + list: [...map.values()], + halfed, + } + }, +) + +export const selectBlessedPrimaryAttribute = createSelector( + selectActiveBlessedTradition, + selectStaticAttributes, + selectDynamicAttributes, + ( + activeBlessedTradition, + staticAttributes, + dynamicAttributes, + ): DisplayedPrimaryAttribute | undefined => { + const id = activeBlessedTradition?.static.primary?.id.attribute + + if (id === undefined) { + return undefined + } else { + const staticAttribute = staticAttributes[id] + const dynamicAttribute = dynamicAttributes[id] ?? createInitialDynamicAttribute(id) + + if (staticAttribute === undefined) { + return undefined + } else { + return { static: staticAttribute, dynamic: dynamicAttribute } + } + } + }, +) + export type DisplayedAttribute = { static: Attribute dynamic: Rated @@ -26,10 +148,7 @@ export const selectTotalPoints = createSelector( selectStaticAttributes, selectDynamicAttributes, (attributes, dynamicAttributes): number => - Object.values(attributes).reduce( - (sum, { id }) => sum + (dynamicAttributes[id]?.value ?? 8), - 0 - ) + Object.values(attributes).reduce((sum, { id }) => sum + (dynamicAttributes[id]?.value ?? 8), 0), ) export const selectVisibleAttributes = createSelector( @@ -44,6 +163,15 @@ export const selectVisibleAttributes = createSelector( createPropertySelector(selectActiveOptionalRules, OptionalRuleIdentifier.MaximumAttributeScores), selectAttributeAdjustmentId, selectDerivedCharacteristics, + selectSkills, + selectCloseCombatTechniques, + selectRangedCombatTechniques, + selectSpells, + selectRituals, + selectLiturgicalChants, + selectCeremonies, + selectHighestMagicalPrimaryAttributes, + selectBlessedPrimaryAttribute, ( attributes, dynamicAttributes, @@ -56,16 +184,49 @@ export const selectVisibleAttributes = createSelector( maximumAttributeScores, attributeAdjustmentId, derivedCharacteristics, - ): DisplayedAttribute[] => - Object.values(attributes) + skills, + closeCombatTechniques, + rangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + highestMagicalPrimaryAttributes, + blessedPrimaryAttribute, + ): DisplayedAttribute[] => { + const filterApplyingDependencies = filterApplyingRatedDependencies({ + attributes: dynamicAttributes, + skills, + closeCombatTechniques, + rangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + }) + + const singleHighestMagicalPrimaryAttribute = + highestMagicalPrimaryAttributes.list.length === 1 + ? highestMagicalPrimaryAttributes.list[0] + : undefined + + return Object.values(attributes) .sort((a, b) => a.id - b.id) .map(attribute => { const dynamicAttribute = dynamicAttributes[attribute.id] ?? createInitialDynamicAttribute(attribute.id) const minimum = getAttributeMinimum( - derivedCharacteristics, + derivedCharacteristics.lifePoints, + derivedCharacteristics.arcaneEnergy, + derivedCharacteristics.karmaPoints, dynamicAttribute, + singleHighestMagicalPrimaryAttribute?.static.id, + [], // TODO: Replace + blessedPrimaryAttribute?.static.id, + [], // TODO: Replace + filterApplyingDependencies, + _id => undefined, // TODO: Replace ) const maximum = getAttributeMaximum( @@ -83,21 +244,17 @@ export const selectVisibleAttributes = createSelector( dynamic: dynamicAttribute, minimum, maximum, - isDecreasable: - isAttributeDecreasable( - dynamicAttribute, - minimum - ), - isIncreasable: - isAttributeIncreasable( - dynamicAttribute, - maximum, - totalPoints, - maxTotalPoints, - isInCharacterCreation, - ), + isDecreasable: isAttributeDecreasable(dynamicAttribute, minimum), + isIncreasable: isAttributeIncreasable( + dynamicAttribute, + maximum, + totalPoints, + maxTotalPoints, + isInCharacterCreation, + ), } }) + }, ) // const getAddedEnergies = createMaybeSelector ( @@ -120,104 +277,6 @@ export const selectVisibleAttributes = createSelector( // maximum // ) -export type DisplayedPrimaryAttribute = { - static: Attribute - dynamic: Rated -} - -export type DisplayedMagicalPrimaryAttributes = { - list: DisplayedPrimaryAttribute[] - halfed: boolean -} - -export const selectHighestMagicalPrimaryAttributes = createSelector( - selectActiveMagicalTraditions, - selectStaticAttributes, - selectDynamicAttributes, - ( - activeMagicalTraditions, - staticAttributes, - dynamicAttributes - ): DisplayedMagicalPrimaryAttributes => { - const { map, halfed } = activeMagicalTraditions - .reduce<{ map: Map; halfed: boolean }>( - (currentlyHighest, magicalTradition) => { - const staticPrimaryAttribute = magicalTradition.static.primary - - if (staticPrimaryAttribute === undefined) { - return currentlyHighest - } - else { - const { id: { attribute: id }, use_half_for_arcane_energy } = staticPrimaryAttribute - const staticAttribute = staticAttributes[id] - const dynamicAttribute = dynamicAttributes[id] ?? createInitialDynamicAttribute(id) - - if (staticAttribute === undefined) { - return currentlyHighest - } - else if ( - currentlyHighest.map.size === 0 - || [ ...currentlyHighest.map.values() ][0]!.dynamic.value < dynamicAttribute.value - ) { - return { - map: new Map([ - [ - id, - { static: staticAttribute, dynamic: dynamicAttribute }, - ], - ]), - halfed: currentlyHighest.halfed || use_half_for_arcane_energy, - } - } - else { - return { - map: currentlyHighest.map.set( - id, - { static: staticAttribute, dynamic: dynamicAttribute } - ), - halfed: currentlyHighest.halfed || use_half_for_arcane_energy, - } - } - } - }, - { map: new Map(), halfed: false } - ) - - return { - list: [ ...map.values() ], - halfed, - } - } -) - -export const selectBlessedPrimaryAttribute = createSelector( - selectActiveBlessedTradition, - selectStaticAttributes, - selectDynamicAttributes, - ( - activeBlessedTradition, - staticAttributes, - dynamicAttributes, - ): DisplayedPrimaryAttribute | undefined => { - const id = activeBlessedTradition?.static.primary?.id.attribute - - if (id === undefined) { - return undefined - } - else { - const staticAttribute = staticAttributes[id] - const dynamicAttribute = dynamicAttributes[id] ?? createInitialDynamicAttribute(id) - - if (staticAttribute === undefined) { - return undefined - } - else { - return { static: staticAttribute, dynamic: dynamicAttribute } - } - } - } -) - // export const getPrimaryMagicalAttributes = createMaybeSelector ( // getWikiAttributes, // getAttributes, @@ -269,7 +328,7 @@ export const selectBlessedPrimaryAttribute = createSelector( export const selectCarryingCapacity = createSelector( selectDynamicAttributes, - (attributes): number => (attributes[8]?.value ?? 8) * 2 + (attributes): number => (attributes[8]?.value ?? 8) * 2, ) export const selectAvailableAdjustments = createSelector( @@ -277,25 +336,24 @@ export const selectAvailableAdjustments = createSelector( selectAttributeAdjustmentId, selectVisibleAttributes, (race, currentId, attributes) => { - const selectableAdjustment = race?.attribute_adjustments.find(pair => pair.list.length > 1) + const selectableAdjustment = race?.attribute_adjustments?.selectable?.[0] if (selectableAdjustment === undefined) { return undefined - } - else { + } else { const current = attributes.find(attr => attr.static.id === currentId) - const canNotSwitch = current !== undefined - && current.maximum !== undefined - && current.maximum - selectableAdjustment.value < attributeValue(current.dynamic) + const canNotSwitch = + current !== undefined && + current.maximum !== undefined && + current.maximum - selectableAdjustment.value < attributeValue(current.dynamic) if (canNotSwitch) { return { value: selectableAdjustment.value, - list: [ current ], + list: [current], } - } - else { + } else { return { value: selectableAdjustment.value, list: selectableAdjustment.list @@ -304,5 +362,5 @@ export const selectAvailableAdjustments = createSelector( } } } - } + }, ) diff --git a/src/main_window/selectors/combatTechniquesSelectors.ts b/src/main_window/selectors/combatTechniquesSelectors.ts new file mode 100644 index 00000000..c27a9195 --- /dev/null +++ b/src/main_window/selectors/combatTechniquesSelectors.ts @@ -0,0 +1,368 @@ +import { createSelector } from "@reduxjs/toolkit" +import { CloseCombatTechnique } from "optolith-database-schema/types/CombatTechnique_Close" +import { RangedCombatTechnique } from "optolith-database-schema/types/CombatTechnique_Ranged" +// import { CombatTechniqueId, SpecialAbilityId } from "../../App/Constants/Ids.ts" +// import { createSkillDependentWithValue6 } from "../../App/Models/ActiveEntries/SkillDependent.ts" +// import { HeroModel, HeroModelRecord } from "../../App/Models/Hero/HeroModel.ts" +// import { +// CombatTechniqueWithAttackParryBase, +// CombatTechniqueWithAttackParryBaseA_, +// } from "../../App/Models/View/CombatTechniqueWithAttackParryBase.ts" +// import { CombatTechniqueWithRequirements } from "../../App/Models/View/CombatTechniqueWithRequirements.ts" +// import { CombatTechnique } from "../../App/Models/Wiki/CombatTechnique.ts" +// import { StaticData } from "../../App/Models/Wiki/WikiModel.ts" +// import { getRuleBooksEnabled } from "../../App/Selectors/rulesSelectors.ts" +// import { getCombatTechniquesWithRequirementsSortOptions } from "../../App/Selectors/sortOptionsSelectors.ts" +// import { +// getAttributes, +// getCombatTechniques, +// getCombatTechniquesFilterText, +// getCurrentHeroPresent, +// getSpecialAbilities, +// getWiki, +// getWikiCombatTechniques, +// } from "../../App/Selectors/stateSelectors.ts" +// import { isMaybeActive } from "../../App/Utilities/Activatable/isActive.ts" +// import { compareLocale } from "../../App/Utilities/I18n.ts" +// import { +// isDecreaseDisabled, +// isIncreaseDisabled, +// } from "../../App/Utilities/Increasable/combatTechniqueUtils.ts" +// import { filterByAvailabilityAndPred, isEntryFromCoreBook } from "../../App/Utilities/RulesUtils.ts" +// import { createMaybeSelector } from "../../App/Utilities/createMaybeSelector.ts" +// import { filterAndSortRecordsBy } from "../../App/Utilities/filterAndSortBy.ts" +// import { pipe, pipe_ } from "../../App/Utilities/pipe.ts" +// import { comparingR, sortByMulti } from "../../App/Utilities/sortBy.ts" +// import { ident, thrush } from "../../Data/Function.ts" +// import { fmap, fmapF } from "../../Data/Functor.ts" +// import { List, consF, filter, fnull, map } from "../../Data/List.ts" +// import { liftM2, maybe } from "../../Data/Maybe.ts" +// import { gt } from "../../Data/Num.ts" +// import { findWithDefault, foldrWithKey, lookup } from "../../Data/OrderedMap.ts" +// import { Record } from "../../Data/Record.ts" +// import { uncurryN } from "../../Data/Tuple/Curry.ts" +import { isActive } from "../../shared/domain/activatableEntry.ts" +import { + getAttackBaseForClose, + getAttackBaseForRanged, + getCombatTechniqueMaximum, + getCombatTechniqueMinimum, + getParryBaseForClose, + isCombatTechniqueDecreasable, + isCombatTechniqueIncreasable, +} from "../../shared/domain/combatTechnique.ts" +import { filterApplyingRatedDependencies } from "../../shared/domain/dependencies/filterApplyingDependencies.ts" +import { + AdvantageIdentifier, + GeneralSpecialAbilityIdentifier, + RangedCombatTechniqueIdentifier, +} from "../../shared/domain/identifier.ts" +import { Rated } from "../../shared/domain/ratedEntry.ts" +import { isNotNullish } from "../../shared/utils/nullable.ts" +import { createPropertySelector } from "../../shared/utils/redux.ts" +import { + selectAdvantages, + selectAttributes, + selectCeremonies, + selectCloseCombatTechniques as selectDynamicCloseCombatTechniques, + selectRangedCombatTechniques as selectDynamicRangedCombatTechniques, + selectGeneralSpecialAbilities, + selectLiturgicalChants, + selectRituals, + selectSkills, + selectSpells, +} from "../slices/characterSlice.ts" +import { createInitialDynamicCloseCombatTechnique } from "../slices/closeCombatTechniqueSlice.ts" +import { + selectCloseCombatTechniques as selectStaticCloseCombatTechniques, + selectRangedCombatTechniques as selectStaticRangedCombatTechniques, +} from "../slices/databaseSlice.ts" +import { createInitialDynamicRangedCombatTechnique } from "../slices/rangedCombatTechniqueSlice.ts" +import { selectCanRemove, selectIsInCharacterCreation } from "./characterSelectors.ts" +import { selectStartExperienceLevel } from "./experienceLevelSelectors.ts" + +export type DisplayedCloseCombatTechnique = { + kind: "close" + static: CloseCombatTechnique + dynamic: Rated + minimum: number + maximum: number + isIncreasable: boolean + isDecreasable: boolean + attackBase: number + parryBase?: number +} + +export type DisplayedRangedCombatTechnique = { + kind: "ranged" + static: RangedCombatTechnique + dynamic: Rated + minimum: number + maximum: number + isIncreasable: boolean + isDecreasable: boolean + attackBase: number +} + +export type DisplayedCombatTechnique = + | DisplayedCloseCombatTechnique + | DisplayedRangedCombatTechnique + +export const selectVisibleCloseCombatTechniques = createSelector( + selectStaticCloseCombatTechniques, + selectDynamicCloseCombatTechniques, + selectIsInCharacterCreation, + selectStartExperienceLevel, + selectCanRemove, + createPropertySelector(selectAdvantages, AdvantageIdentifier.ExceptionalSkill), + createPropertySelector(selectGeneralSpecialAbilities, GeneralSpecialAbilityIdentifier.Hunter), + selectAttributes, + selectSkills, + selectDynamicRangedCombatTechniques, + selectSpells, + selectRituals, + selectLiturgicalChants, + selectCeremonies, + ( + staticCloseCombatTechniques, + dynamicCloseCombatTechniques, + isInCharacterCreation, + startExperienceLevel, + canRemove, + exceptionalSkill, + hunter, + attributes, + skills, + rangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + ): DisplayedCloseCombatTechnique[] => { + const filterApplyingDependencies = filterApplyingRatedDependencies({ + attributes, + skills, + closeCombatTechniques: dynamicCloseCombatTechniques, + rangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + }) + + return Object.values(staticCloseCombatTechniques).map(combatTechnique => { + const dynamicCloseCombatTechnique = + dynamicCloseCombatTechniques[combatTechnique.id] ?? + createInitialDynamicCloseCombatTechnique(combatTechnique.id) + + const minimum = getCombatTechniqueMinimum( + rangedCombatTechniques, + { tag: "CloseCombatTechnique", closeCombatTechnique: combatTechnique }, + dynamicCloseCombatTechnique, + hunter, + filterApplyingDependencies, + ) + + const maximum = getCombatTechniqueMaximum( + attributes, + { tag: "CloseCombatTechnique", closeCombatTechnique: combatTechnique }, + isInCharacterCreation, + startExperienceLevel, + exceptionalSkill, + ) + + return { + kind: "close", + static: combatTechnique, + dynamic: dynamicCloseCombatTechnique, + minimum, + maximum, + isDecreasable: isCombatTechniqueDecreasable( + dynamicCloseCombatTechnique, + minimum, + canRemove, + ), + isIncreasable: isCombatTechniqueIncreasable(dynamicCloseCombatTechnique, maximum), + attackBase: getAttackBaseForClose(attributes, dynamicCloseCombatTechnique), + parryBase: getParryBaseForClose(attributes, combatTechnique, dynamicCloseCombatTechnique), + } + }) + }, +) + +export const selectVisibleRangedCombatTechniques = createSelector( + selectStaticRangedCombatTechniques, + selectDynamicRangedCombatTechniques, + selectIsInCharacterCreation, + selectStartExperienceLevel, + selectCanRemove, + createPropertySelector(selectAdvantages, AdvantageIdentifier.ExceptionalSkill), + createPropertySelector(selectGeneralSpecialAbilities, GeneralSpecialAbilityIdentifier.Hunter), + createPropertySelector(selectGeneralSpecialAbilities, GeneralSpecialAbilityIdentifier.FireEater), + selectAttributes, + selectSkills, + selectDynamicCloseCombatTechniques, + selectSpells, + selectRituals, + selectLiturgicalChants, + selectCeremonies, + ( + staticRangedCombatTechniques, + dynamicRangedCombatTechniques, + isInCharacterCreation, + startExperienceLevel, + canRemove, + exceptionalSkill, + hunter, + fireEater, + attributes, + skills, + closeCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + ): DisplayedRangedCombatTechnique[] => { + const filterApplyingDependencies = filterApplyingRatedDependencies({ + attributes, + skills, + closeCombatTechniques, + rangedCombatTechniques: dynamicRangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + }) + + const isFireEaterActive = isActive(fireEater) + + return Object.values(staticRangedCombatTechniques) + .map(combatTechnique => { + if ( + combatTechnique.id === RangedCombatTechniqueIdentifier.SpittingFire && + !isFireEaterActive + ) { + return undefined + } + + const dynamicRangedCombatTechnique = + dynamicRangedCombatTechniques[combatTechnique.id] ?? + createInitialDynamicRangedCombatTechnique(combatTechnique.id) + + const minimum = getCombatTechniqueMinimum( + dynamicRangedCombatTechniques, + { tag: "RangedCombatTechnique", rangedCombatTechnique: combatTechnique }, + dynamicRangedCombatTechnique, + hunter, + filterApplyingDependencies, + ) + + const maximum = getCombatTechniqueMaximum( + attributes, + { tag: "RangedCombatTechnique", rangedCombatTechnique: combatTechnique }, + isInCharacterCreation, + startExperienceLevel, + exceptionalSkill, + ) + + return { + kind: "ranged" as const, + static: combatTechnique, + dynamic: dynamicRangedCombatTechnique, + minimum, + maximum, + isDecreasable: isCombatTechniqueDecreasable( + dynamicRangedCombatTechnique, + minimum, + canRemove, + ), + isIncreasable: isCombatTechniqueIncreasable(dynamicRangedCombatTechnique, maximum), + attackBase: getAttackBaseForRanged( + attributes, + combatTechnique, + dynamicRangedCombatTechnique, + ), + } + }) + .filter(isNotNullish) + }, +) + +export const selectVisibleCombatTechniques = createSelector( + selectVisibleCloseCombatTechniques, + selectVisibleRangedCombatTechniques, + (closeCombatTechniques, rangedCombatTechniques): DisplayedCombatTechnique[] => [ + ...closeCombatTechniques, + ...rangedCombatTechniques, + ], +) + +// export const getCombatTechniquesForSheet = createMaybeSelector( +// getWiki, +// getCombatTechniquesForView, +// (staticData, combatTechniques) => +// fmapF(combatTechniques)( +// filter( +// x => +// SDA.value(CTWAPBA.stateEntry(x)) > 6 || +// isEntryFromCoreBook(CTA.src)(StaticData.A.books(staticData))(CTWAPBA.wikiEntry(x)), +// ), +// ), +// ) + +// const getGr = pipe(CTWAPBA.wikiEntry, CTA.gr) +// const getValue = pipe(CTWAPBA.stateEntry, SDA.value) +// type CTWAPB = CombatTechniqueWithAttackParryBase + +// export const getAllCombatTechniques = createMaybeSelector( +// getCombatTechniquesForView, +// getCurrentHeroPresent, +// getWiki, +// (mcombat_techniques, mhero, wiki) => +// liftM2((combatTechniques: List>) => (hero: HeroModelRecord) => { +// const hunter = lookup(SpecialAbilityId.Hunter)(HeroModel.A.specialAbilities(hero)) + +// const hunterRequiresMinimum = +// isMaybeActive(hunter) && +// thrush(combatTechniques)(List.any(x => getGr(x) === 2 && getValue(x) >= 10)) + +// return thrush(combatTechniques)( +// map(x => +// CombatTechniqueWithRequirements({ +// at: CTWAPBA.at(x), +// pa: CTWAPBA.pa(x), +// isDecreasable: !isDecreaseDisabled(wiki)(hero)(CTWAPBA.wikiEntry(x))( +// CTWAPBA.stateEntry(x), +// )(hunterRequiresMinimum), +// isIncreasable: !isIncreaseDisabled(wiki)(hero)(CTWAPBA.wikiEntry(x))( +// CTWAPBA.stateEntry(x), +// ), +// stateEntry: CTWAPBA.stateEntry(x), +// wikiEntry: CTWAPBA.wikiEntry(x), +// }), +// ), +// ) +// })(mcombat_techniques)(mhero), +// ) + +// export const getAvailableCombatTechniques = createMaybeSelector( +// getRuleBooksEnabled, +// getAllCombatTechniques, +// uncurryN(av => +// fmap( +// filterByAvailabilityAndPred(pipe(CTWRA.wikiEntry, CTA.src))( +// pipe(CTWRA.stateEntry, SDA.value, gt(6)), +// )(av), +// ), +// ), +// ) + +// export const getFilteredCombatTechniques = createMaybeSelector( +// getAvailableCombatTechniques, +// getCombatTechniquesWithRequirementsSortOptions, +// getCombatTechniquesFilterText, +// (mcombat_techniques, sortOptions, filterText) => +// fmapF(mcombat_techniques)( +// filterAndSortRecordsBy(0)([pipe(CTWRA.wikiEntry, CTA.name)])(sortOptions)(filterText), +// ), +// ) diff --git a/src/main_window/selectors/magicalTraditionSelectors.ts b/src/main_window/selectors/magicalTraditionSelectors.ts index 6102989c..21eee797 100644 --- a/src/main_window/selectors/magicalTraditionSelectors.ts +++ b/src/main_window/selectors/magicalTraditionSelectors.ts @@ -1,10 +1,16 @@ import { createSelector } from "@reduxjs/toolkit" import { BlessedTradition } from "optolith-database-schema/types/specialAbility/BlessedTradition" import { MagicalTradition } from "optolith-database-schema/types/specialAbility/MagicalTradition" -import { isActive } from "../../shared/domain/activatableEntry.ts" +import { Activatable, isActive } from "../../shared/domain/activatableEntry.ts" import { isNotNullish } from "../../shared/utils/nullable.ts" -import { Activatable, selectBlessedTraditions as selectDynamicBlessedTraditions, selectMagicalTraditions as selectDynamicMagicalTraditions } from "../slices/characterSlice.ts" -import { selectBlessedTraditions as selectStaticBlessedTraditions, selectMagicalTraditions as selectStaticMagicalTraditions } from "../slices/databaseSlice.ts" +import { + selectBlessedTraditions as selectDynamicBlessedTraditions, + selectMagicalTraditions as selectDynamicMagicalTraditions, +} from "../slices/characterSlice.ts" +import { + selectBlessedTraditions as selectStaticBlessedTraditions, + selectMagicalTraditions as selectStaticMagicalTraditions, +} from "../slices/databaseSlice.ts" export type CombinedActiveMagicalTradition = { static: MagicalTradition @@ -28,7 +34,7 @@ export const selectActiveMagicalTraditions = createSelector( return undefined }) - .filter(isNotNullish) + .filter(isNotNullish), ) export type CombinedActiveBlessedTradition = { @@ -53,5 +59,5 @@ export const selectActiveBlessedTradition = createSelector( return undefined }) - .find(isNotNullish) + .find(isNotNullish), ) diff --git a/src/main_window/selectors/personalDataSelectors.ts b/src/main_window/selectors/personalDataSelectors.ts index c71a5d8d..56f19061 100644 --- a/src/main_window/selectors/personalDataSelectors.ts +++ b/src/main_window/selectors/personalDataSelectors.ts @@ -4,7 +4,11 @@ import { HairColor } from "optolith-database-schema/types/HairColor" import { Height, Weight } from "optolith-database-schema/types/Race" import { SocialStatus } from "optolith-database-schema/types/SocialStatus" import { isOptionActive } from "../../shared/domain/activatableEntry.ts" -import { DisadvantageIdentifier, EyeColorIdentifier, HairColorIdentifier } from "../../shared/domain/identifier.ts" +import { + DisadvantageIdentifier, + EyeColorIdentifier, + HairColorIdentifier, +} from "../../shared/domain/identifier.ts" import { filterNonNullable, unique } from "../../shared/utils/array.ts" import { compareAt, numAsc } from "../../shared/utils/compare.ts" import { createPropertySelector } from "../../shared/utils/redux.ts" @@ -20,15 +24,17 @@ export const selectAvailableSocialStatuses = createSelector( (currentCulture, socialStatusDependencies, socialStatuses): SocialStatus[] => { const minimumSocialStatus = socialStatusDependencies.length === 0 - ? 1 - : Math.max(...socialStatusDependencies.map(dep => dep.id)) + ? 1 + : Math.max(...socialStatusDependencies.map(dep => dep.id)) return Object.values(socialStatuses) .sort(compareAt(status => status.id, numAsc)) - .filter(status => - status.id >= minimumSocialStatus - && currentCulture?.social_status.some(ref => ref.id.social_status === status.id)) - } + .filter( + status => + status.id >= minimumSocialStatus && + currentCulture?.social_status.some(ref => ref.id.social_status === status.id), + ) + }, ) const ALBINO = 1 @@ -47,26 +53,24 @@ export const selectAvailableHairColorsIdDice = createSelector( isAlbino ? HairColorIdentifier.White : undefined, isGreenHaired ? HairColorIdentifier.Green : undefined, ]) - } - else if (currentRace === undefined) { + } else if (currentRace === undefined) { return [] + } else { + return ( + currentRaceVariant?.hair_color?.map(ref => ref.id.hair_color) ?? + (currentRace.variants.length === 1 + ? currentRace.variants[0]!.hair_color.map(ref => ref.id.hair_color) + : []) + ) } - else { - return currentRaceVariant?.hair_color?.map(ref => ref.id.hair_color) - ?? ( - currentRace.variant_dependent.tag === "Plain" - ? currentRace.variant_dependent.plain.hair_color.map(ref => ref.id.hair_color) - : [] - ) - } - } + }, ) export const selectAvailableHairColors = createSelector( selectAvailableHairColorsIdDice, selectHairColors, (hairColorIds, hairColors): HairColor[] => - filterNonNullable(unique(hairColorIds).map(id => hairColors[id])) + filterNonNullable(unique(hairColorIds).map(id => hairColors[id])), ) export const selectAvailableEyeColorsIdDice = createSelector( @@ -77,43 +81,39 @@ export const selectAvailableEyeColorsIdDice = createSelector( const isAlbino = isOptionActive(stigma, { type: "Generic", value: ALBINO }) if (isAlbino) { - const eyeColorIds = [ EyeColorIdentifier.Red, EyeColorIdentifier.Purple ] + const eyeColorIds = [EyeColorIdentifier.Red, EyeColorIdentifier.Purple] return eyeColorIds - } - else if (currentRace === undefined) { + } else if (currentRace === undefined) { return [] + } else { + return ( + currentRaceVariant?.eye_color?.map(ref => ref.id.eye_color) ?? + (currentRace.variants.length === 1 + ? currentRace.variants[0]!.eye_color.map(ref => ref.id.eye_color) + : []) + ) } - else { - return currentRaceVariant?.eye_color?.map(ref => ref.id.eye_color) - ?? ( - currentRace.variant_dependent.tag === "Plain" - ? currentRace.variant_dependent.plain.eye_color.map(ref => ref.id.eye_color) - : [] - ) - } - } + }, ) export const selectAvailableEyeColors = createSelector( selectAvailableEyeColorsIdDice, selectEyeColors, (eyeColorIds, eyeColors): EyeColor[] => - filterNonNullable(unique(eyeColorIds).map(id => eyeColors[id])) + filterNonNullable(unique(eyeColorIds).map(id => eyeColors[id])), ) export const selectRandomHeightCalculation = createSelector( selectCurrentRace, selectCurrentRaceVariant, (currentRace, currentRaceVariant): Height => - currentRaceVariant?.height - ?? ( - currentRace?.variant_dependent.tag === "Plain" - ? currentRace.variant_dependent.plain.height - : { base: 0, random: [] } - ) + currentRaceVariant?.height ?? + (currentRace?.variants.length === 1 + ? currentRace.variants[0]!.height + : { base: 0, random: [] }), ) export const selectRandomWeightCalculation = createSelector( selectCurrentRace, - (currentRace): Weight => currentRace?.weight ?? { base: 0, random: [] } + (currentRace): Weight => currentRace?.weight ?? { base: 0, random: [] }, ) diff --git a/src/main_window/selectors/skillsSelectors.ts b/src/main_window/selectors/skillsSelectors.ts index f87b7335..c6a0d794 100644 --- a/src/main_window/selectors/skillsSelectors.ts +++ b/src/main_window/selectors/skillsSelectors.ts @@ -1,10 +1,31 @@ import { createSelector } from "@reduxjs/toolkit" import { Skill } from "optolith-database-schema/types/Skill" -import { AdvantageIdentifier } from "../../shared/domain/identifier.ts" +import { filterApplyingRatedDependencies } from "../../shared/domain/dependencies/filterApplyingDependencies.ts" +import { + AdvantageIdentifier, + GeneralSpecialAbilityIdentifier, +} from "../../shared/domain/identifier.ts" import { Rated } from "../../shared/domain/ratedEntry.ts" -import { getSkillCommonness, getSkillMaximum, getSkillMinimum, isSkillDecreasable, isSkillIncreasable } from "../../shared/domain/skill.ts" +import { + getSkillCommonness, + getSkillMaximum, + getSkillMinimum, + isSkillDecreasable, + isSkillIncreasable, +} from "../../shared/domain/skill.ts" import { createPropertySelector } from "../../shared/utils/redux.ts" -import { selectAdvantages, selectAttributes, selectSkills as selectDynamicSkills } from "../slices/characterSlice.ts" +import { + selectAdvantages, + selectAttributes, + selectCeremonies, + selectCloseCombatTechniques, + selectSkills as selectDynamicSkills, + selectGeneralSpecialAbilities, + selectLiturgicalChants, + selectRangedCombatTechniques, + selectRituals, + selectSpells, +} from "../slices/characterSlice.ts" import { selectSkills as selectStaticSkills } from "../slices/databaseSlice.ts" import { createInitialDynamicSkill } from "../slices/skillsSlice.ts" import { selectCanRemove, selectIsInCharacterCreation } from "./characterSelectors.ts" @@ -29,7 +50,17 @@ export const selectVisibleSkills = createSelector( selectStartExperienceLevel, selectCanRemove, createPropertySelector(selectAdvantages, AdvantageIdentifier.ExceptionalSkill), + createPropertySelector( + selectGeneralSpecialAbilities, + GeneralSpecialAbilityIdentifier.CraftInstruments, + ), selectCurrentCulture, + selectCloseCombatTechniques, + selectRangedCombatTechniques, + selectSpells, + selectRituals, + selectLiturgicalChants, + selectCeremonies, ( skills, dynamicSkills, @@ -38,41 +69,53 @@ export const selectVisibleSkills = createSelector( startExperienceLevel, canRemove, exceptionalSkill, + craftInstruments, culture, - ): DisplayedSkill[] => - Object.values(skills) - .sort((a, b) => a.id - b.id) - .map(skill => { - const dynamicSkill = - dynamicSkills[skill.id] ?? createInitialDynamicSkill(skill.id) + closeCombatTechniques, + rangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + ): DisplayedSkill[] => { + const filterApplyingDependencies = filterApplyingRatedDependencies({ + attributes, + skills: dynamicSkills, + closeCombatTechniques, + rangedCombatTechniques, + spells, + rituals, + liturgicalChants, + ceremonies, + }) - const minimum = getSkillMinimum() + return Object.values(skills).map(skill => { + const dynamicSkill = dynamicSkills[skill.id] ?? createInitialDynamicSkill(skill.id) - const maximum = getSkillMaximum( - attributes, - skill, - isInCharacterCreation, - startExperienceLevel, - exceptionalSkill, - ) + const minimum = getSkillMinimum( + dynamicSkills, + dynamicSkill, + craftInstruments, + filterApplyingDependencies, + ) - return { - static: skill, - dynamic: dynamicSkill, - minimum, - maximum, - isDecreasable: - isSkillDecreasable( - dynamicSkill, - minimum, - canRemove, - ), - isIncreasable: - isSkillIncreasable( - dynamicSkill, - maximum, - ), - commonness: culture === undefined ? undefined : getSkillCommonness(culture, skill), - } - }) + const maximum = getSkillMaximum( + attributes, + skill, + isInCharacterCreation, + startExperienceLevel, + exceptionalSkill, + ) + + return { + static: skill, + dynamic: dynamicSkill, + minimum, + maximum, + isDecreasable: isSkillDecreasable(dynamicSkill, minimum, canRemove), + isIncreasable: isSkillIncreasable(dynamicSkill, maximum), + commonness: culture === undefined ? undefined : getSkillCommonness(culture, skill), + } + }) + }, ) diff --git a/src/main_window/slices/characterSlice.ts b/src/main_window/slices/characterSlice.ts index e695e4b9..4c59f1ae 100644 --- a/src/main_window/slices/characterSlice.ts +++ b/src/main_window/slices/characterSlice.ts @@ -1,15 +1,18 @@ -/* eslint-disable max-len */ import { AnyAction, createAction } from "@reduxjs/toolkit" import { Draft } from "immer" -import { ActivatableRated, ActivatableRatedWithEnhancements, Rated } from "../../shared/domain/ratedEntry.ts" +import { ActivatableMap } from "../../shared/domain/activatableEntry.ts" +import { ActivatableRatedMap, ActivatableRatedWithEnhancementsMap, RatedMap } from "../../shared/domain/ratedEntry.ts" +import { Sex } from "../../shared/domain/sex.ts" import { createImmerReducer, reduceReducers } from "../../shared/utils/redux.ts" import { RootState } from "../store.ts" import { attributesReducer } from "./attributesSlice.ts" +import { closeCombatTechniquesReducer } from "./closeCombatTechniqueSlice.ts" import { DatabaseState } from "./databaseSlice.ts" import { derivedCharacteristicsReducer } from "./derivedCharacteristicsSlice.ts" import { personalDataReducer } from "./personalDataSlice.ts" import { professionReducer } from "./professionSlice.ts" import { raceReducer } from "./raceSlice.ts" +import { rangedCombatTechniquesReducer } from "./rangedCombatTechniqueSlice.ts" import { rulesReducer } from "./rulesSlice.ts" import { skillsReducer } from "./skillsSlice.ts" @@ -355,65 +358,6 @@ export type ActiveOptionalRule = { options?: number[] } -/** - * The character's sex. It does not have to be binary, although it always must be specified how to handle it in the context of binary sex prerequisites. You can also provide a custom sex with a custom name. - */ -export type Sex = - | BinarySex - | NonBinarySex - | CustomSex - -/** - * A binary sex option. - */ -export type BinarySex = { - type: "Male" | "Female" -} - -/** - * A non-binary sex option. - */ -export type NonBinarySex ={ - type: "BalThani" | "Tsajana" - - /** - * Defines how a non-binary sex should be treated when checking prerequisites. - */ - binaryHandling: BinaryHandling -} - -/** - * A custom non-binary sex option. - */ -export type CustomSex = { - type: "Custom" - - /** - * The custom sex name. - */ - name: string - - /** - * Defines how a non-binary sex should be treated when checking prerequisites. - */ - binaryHandling: BinaryHandling -} - -/** - * Defines how a non-binary sex should be treated when checking prerequisites. - */ -export type BinaryHandling = { - /** - * Defines if the sex should be treated as male when checking prerequisites. - */ - asMale: boolean - - /** - * Defines if the sex should be treated as female when checking prerequisites. - */ - asFemale: boolean -} - export type SocialStatusDependency = { id: number } @@ -475,18 +419,6 @@ export type EnergyWithBuyBack = { permanentlyLostBoughtBack: number } -export type RatedMap = { - [id: number]: Rated -} - -export type ActivatableRatedMap = { - [id: number]: ActivatableRated -} - -export type ActivatableRatedWithEnhancementsMap = { - [id: number]: ActivatableRatedWithEnhancements -} - export type TinyActivatableSet = number[] /** @@ -523,96 +455,6 @@ export type Purse = { ducats: number } -export type Activatable = { - /** - * The activatable identifier. - * @integer - */ - id: number - - /** - * One or multiple activations of the activatable. - */ - instances: { - /** - * One or multiple options for the activatable. The meaning depends on the activatable. - * @minItems 1 - */ - options?: ActivatableOption[] - - /** - * The instance level (if the activatable has levels). - */ - level?: number - - /** - * If provided, a custom adventure points value has been set for this instance. - */ - customAdventurePointsValue?: number - }[] -} - -export type ActivatableOption = - | PredefinedActivatableOption - | CustomActivatableOption - -export type PredefinedActivatableOption = { - type: "Predefined" - - /** - * 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" - | "MeleeCombatTechnique" - | "RangedCombatTechnique" - | "LiturgicalChant" - | "Ceremony" - | "Spell" - | "Ritual" - - /** - * The numeric identifier. - */ - value: number - } -} - -export type CustomActivatableOption = { - type: "Custom" - - /** - * A user-entered text. - */ - value: string -} - -export type ActivatableMap = { - [id: number]: Activatable -} - const staticInitialState: Omit = { id: "550e8400-e29b-11d4-a716-446655440000", version: undefined, @@ -867,6 +709,12 @@ export const selectPurchasedKarmaPoints = (state: RootState) => selectCurrentCha export const selectKarmaPointsPermanentlyLost = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.karmaPoints.permanentlyLost ?? 0 export const selectKarmaPointsPermanentlyLostBoughtBack = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack ?? 0 export const selectSkills = (state: RootState) => selectCurrentCharacter(state)?.skills ?? {} +export const selectCloseCombatTechniques = (state: RootState) => selectCurrentCharacter(state)?.combatTechniques.close ?? {} +export const selectRangedCombatTechniques = (state: RootState) => selectCurrentCharacter(state)?.combatTechniques.ranged ?? {} +export const selectSpells = (state: RootState) => selectCurrentCharacter(state)?.spells ?? {} +export const selectRituals = (state: RootState) => selectCurrentCharacter(state)?.rituals ?? {} +export const selectLiturgicalChants = (state: RootState) => selectCurrentCharacter(state)?.liturgicalChants ?? {} +export const selectCeremonies = (state: RootState) => selectCurrentCharacter(state)?.ceremonies ?? {} export const setName = createAction("character/setName") export const setAvatar = createAction("character/setAvatar") @@ -900,4 +748,6 @@ export const characterReducer = professionReducer, rulesReducer, skillsReducer, + closeCombatTechniquesReducer, + rangedCombatTechniquesReducer, ) diff --git a/src/main_window/slices/closeCombatTechniqueSlice.ts b/src/main_window/slices/closeCombatTechniqueSlice.ts new file mode 100644 index 00000000..90687eb2 --- /dev/null +++ b/src/main_window/slices/closeCombatTechniqueSlice.ts @@ -0,0 +1,26 @@ +import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" +import { createRatedSlice } from "./ratedSlice.ts" + +const { + create, + createInitial, + getValue, + actions: { incrementAction, decrementAction }, + reducer, +} = createRatedSlice({ + namespace: "combatTechniques/close", + entityName: "CloseCombatTechnique", + getState: state => state.combatTechniques.close, + minValue: 6, + getImprovementCost: (id, database) => + fromRaw(database.closeCombatTechniques[id]?.improvement_cost) ?? ImprovementCost.D, +}) + +export { + reducer as closeCombatTechniquesReducer, + create as createDynamicCloseCombatTechnique, + createInitial as createInitialDynamicCloseCombatTechnique, + decrementAction as decrementCloseCombatTechnique, + getValue as getCloseCombatTechniqueValue, + incrementAction as incrementCloseCombatTechnique, +} diff --git a/src/main_window/slices/rangedCombatTechniqueSlice.ts b/src/main_window/slices/rangedCombatTechniqueSlice.ts new file mode 100644 index 00000000..fe317e37 --- /dev/null +++ b/src/main_window/slices/rangedCombatTechniqueSlice.ts @@ -0,0 +1,26 @@ +import { ImprovementCost, fromRaw } from "../../shared/domain/adventurePoints/improvementCost.ts" +import { createRatedSlice } from "./ratedSlice.ts" + +const { + create, + createInitial, + getValue, + actions: { incrementAction, decrementAction }, + reducer, +} = createRatedSlice({ + namespace: "combatTechniques/ranged", + entityName: "RangedCombatTechnique", + getState: state => state.combatTechniques.ranged, + minValue: 6, + getImprovementCost: (id, database) => + fromRaw(database.rangedCombatTechniques[id]?.improvement_cost) ?? ImprovementCost.D, +}) + +export { + create as createDynamicRangedCombatTechnique, + createInitial as createInitialDynamicRangedCombatTechnique, + decrementAction as decrementRangedCombatTechnique, + getValue as getRangedCombatTechniqueValue, + incrementAction as incrementRangedCombatTechnique, + reducer as rangedCombatTechniquesReducer, +} diff --git a/src/main_window/slices/ratedSlice.ts b/src/main_window/slices/ratedSlice.ts index 8c15e304..f2a5e673 100644 --- a/src/main_window/slices/ratedSlice.ts +++ b/src/main_window/slices/ratedSlice.ts @@ -1,9 +1,12 @@ import { ActionCreatorWithPayload, AnyAction, Draft, createAction } from "@reduxjs/toolkit" import { ImprovementCost } from "../../shared/domain/adventurePoints/improvementCost.ts" -import { BoundAdventurePoints, cachedAdventurePoints } from "../../shared/domain/adventurePoints/ratedEntry.ts" -import { Dependency, Rated, RatedValue } from "../../shared/domain/ratedEntry.ts" +import { + BoundAdventurePoints, + cachedAdventurePoints, +} from "../../shared/domain/adventurePoints/ratedEntry.ts" +import { Dependency, Rated, RatedMap, RatedValue } from "../../shared/domain/ratedEntry.ts" import { Reducer, createImmerReducer } from "../../shared/utils/redux.ts" -import { CharacterState, RatedMap } from "./characterSlice.ts" +import { CharacterState } from "./characterSlice.ts" import { DatabaseState } from "./databaseSlice.ts" export type RatedSlice = { @@ -80,60 +83,59 @@ export const createRatedSlice = (config: { database, id, value, - { - dependencies = [], - boundAdventurePoints = [], - } = {}, + { dependencies = [], boundAdventurePoints = [] } = {}, ) => - updateCachedAdventurePoints({ - id, - value: Math.max(config.minValue, value), - cachedAdventurePoints: { - general: 0, - bound: 0, + updateCachedAdventurePoints( + { + id, + value: Math.max(config.minValue, value), + cachedAdventurePoints: { + general: 0, + bound: 0, + }, + dependencies, + boundAdventurePoints, }, - dependencies, - boundAdventurePoints, - }, database) + database, + ) const createInitial: RatedSlice["createInitial"] = ( id, - { - dependencies = [], - boundAdventurePoints = [], - } = {}, - ) => - ({ - id, - value: config.minValue, - cachedAdventurePoints: { - general: 0, - bound: 0, - }, - dependencies, - boundAdventurePoints, - }) + { dependencies = [], boundAdventurePoints = [] } = {}, + ) => ({ + id, + value: config.minValue, + cachedAdventurePoints: { + general: 0, + bound: 0, + }, + dependencies, + boundAdventurePoints, + }) const getValue: RatedSlice["getValue"] = entry => entry?.value ?? config.minValue - const incrementAction = createAction(`${config.namespace}/increment${config.entityName}`) - const decrementAction = createAction(`${config.namespace}/decrement${config.entityName}`) + const incrementAction = createAction( + `${config.namespace}/increment${config.entityName}`, + ) + const decrementAction = createAction( + `${config.namespace}/decrement${config.entityName}`, + ) const reducer = createImmerReducer( (state: Draft, action, database: DatabaseState) => { if (incrementAction.match(action)) { - const entry = config.getState(state)[action.payload] ??= createInitial(action.payload) + const entry = (config.getState(state)[action.payload] ??= createInitial(action.payload)) entry.value++ updateCachedAdventurePoints(entry, database) - } - else if (decrementAction.match(action)) { + } else if (decrementAction.match(action)) { const entry = config.getState(state)[action.payload] if (entry !== undefined && entry.value > config.minValue) { entry.value-- updateCachedAdventurePoints(entry, database) } } - } + }, ) return { diff --git a/src/shared/domain/activatableEntry.ts b/src/shared/domain/activatableEntry.ts index b5734304..d1b423f9 100644 --- a/src/shared/domain/activatableEntry.ts +++ b/src/shared/domain/activatableEntry.ts @@ -1,6 +1,93 @@ -import { Activatable, PredefinedActivatableOption } from "../../main_window/slices/characterSlice.ts" import { Equality } from "../utils/compare.ts" +export type Activatable = { + /** + * The activatable identifier. + * @integer + */ + id: number + + /** + * One or multiple activations of the activatable. + */ + instances: { + /** + * One or multiple options for the activatable. The meaning depends on the activatable. + * @minItems 1 + */ + options?: ActivatableOption[] + + /** + * The instance level (if the activatable has levels). + */ + level?: number + + /** + * If provided, a custom adventure points value has been set for this instance. + */ + customAdventurePointsValue?: number + }[] +} + +export type ActivatableOption = PredefinedActivatableOption | CustomActivatableOption + +export type PredefinedActivatableOption = { + type: "Predefined" + + /** + * 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 + } +} + +export type CustomActivatableOption = { + type: "Custom" + + /** + * A user-entered text. + */ + value: string +} + +export type ActivatableMap = { + [id: number]: Activatable +} + const equalOptionId: Equality = (a, b) => a.type === b.type && a.value === b.value diff --git a/src/shared/domain/activatableModifiers.ts b/src/shared/domain/activatableModifiers.ts index c6bd81c3..108c4a30 100644 --- a/src/shared/domain/activatableModifiers.ts +++ b/src/shared/domain/activatableModifiers.ts @@ -1,5 +1,4 @@ -import { Activatable } from "../../main_window/slices/characterSlice.ts" -import { firstLevel, isActive } from "./activatableEntry.ts" +import { Activatable, firstLevel, isActive } from "./activatableEntry.ts" /** * There are pairs of entries that are mutually exclusive and modify a certain @@ -9,8 +8,7 @@ import { firstLevel, isActive } from "./activatableEntry.ts" export const modifierByLevel = ( incrementor: Activatable | undefined, decrementor: Activatable | undefined, -): number => - firstLevel(incrementor) - firstLevel(decrementor) +): number => firstLevel(incrementor) - firstLevel(decrementor) /** * There are pairs of entries that are mutually exclusive and modify a certain @@ -21,8 +19,7 @@ export const modifierByLevel = ( export const modifierByIsActive = ( incrementor: Activatable | undefined, decrementor: Activatable | undefined, -): number => - isActive(incrementor) ? 1 : isActive(decrementor) ? -1 : 0 +): number => (isActive(incrementor) ? 1 : isActive(decrementor) ? -1 : 0) const countActive = (activatables: (Activatable | undefined)[]) => activatables.reduce((acc, entry) => acc + (isActive(entry) ? 1 : 0), 0) @@ -35,5 +32,4 @@ const countActive = (activatables: (Activatable | undefined)[]) => export const modifierByIsActives = ( incrementors: (Activatable | undefined)[], decrementors: (Activatable | undefined)[], -): number => - countActive(incrementors) - countActive(decrementors) +): number => countActive(incrementors) - countActive(decrementors) diff --git a/src/shared/domain/adventurePoints/improvementCost.ts b/src/shared/domain/adventurePoints/improvementCost.ts index 0dcf06f7..ea088190 100644 --- a/src/shared/domain/adventurePoints/improvementCost.ts +++ b/src/shared/domain/adventurePoints/improvementCost.ts @@ -13,12 +13,18 @@ export enum ImprovementCost { const adventureCostBase = (ic: ImprovementCost): number => { switch (ic) { - case ImprovementCost.A: return 1 - case ImprovementCost.B: return 2 - case ImprovementCost.C: return 3 - case ImprovementCost.D: return 4 - case ImprovementCost.E: return 15 - default: return assertExhaustive(ic) + case ImprovementCost.A: + return 1 + case ImprovementCost.B: + return 2 + case ImprovementCost.C: + return 3 + case ImprovementCost.D: + return 4 + case ImprovementCost.E: + return 15 + default: + return assertExhaustive(ic) } } @@ -27,9 +33,12 @@ const constantThresholdValue = (ic: ImprovementCost): number => { case ImprovementCost.A: case ImprovementCost.B: case ImprovementCost.C: - case ImprovementCost.D: return 12 - case ImprovementCost.E: return 14 - default: return assertExhaustive(ic) + case ImprovementCost.D: + return 12 + case ImprovementCost.E: + return 14 + default: + return assertExhaustive(ic) } } @@ -47,7 +56,7 @@ const adventurePointsValue = (ic: ImprovementCost, value: number): number => export const adventurePointsForRange = ( ic: ImprovementCost, fromValue: number, - toValue: number + toValue: number, ): number => { if (fromValue === toValue) { return 0 @@ -89,12 +98,18 @@ export const adventurePointsForActivation = adventureCostBase export const compareImprovementCost = (ic1: ImprovementCost, ic2: ImprovementCost): number => { const toInt = (ic: ImprovementCost): number => { switch (ic) { - case ImprovementCost.A: return 1 - case ImprovementCost.B: return 2 - case ImprovementCost.C: return 3 - case ImprovementCost.D: return 4 - case ImprovementCost.E: return 5 - default: return assertExhaustive(ic) + case ImprovementCost.A: + return 1 + case ImprovementCost.B: + return 2 + case ImprovementCost.C: + return 3 + case ImprovementCost.D: + return 4 + case ImprovementCost.E: + return 5 + default: + return assertExhaustive(ic) } } @@ -111,24 +126,36 @@ export const equals = (ic1: ImprovementCost, ic2: ImprovementCost): boolean => i */ export const toString = (ic: ImprovementCost): string => { switch (ic) { - case ImprovementCost.A: return "A" - case ImprovementCost.B: return "B" - case ImprovementCost.C: return "C" - case ImprovementCost.D: return "D" - case ImprovementCost.E: return "E" - default: return assertExhaustive(ic) + case ImprovementCost.A: + return "A" + case ImprovementCost.B: + return "B" + case ImprovementCost.C: + return "C" + case ImprovementCost.D: + return "D" + case ImprovementCost.E: + return "E" + default: + return assertExhaustive(ic) } } export const fromRaw = ( - ic: T + ic: T, ): ImprovementCost | Nullish => { switch (ic) { - case "A": return ImprovementCost.A - case "B": return ImprovementCost.B - case "C": return ImprovementCost.C - case "D": return ImprovementCost.D - case undefined: return undefined as Nullish - default: return assertExhaustive(ic) + case "A": + return ImprovementCost.A + case "B": + return ImprovementCost.B + case "C": + return ImprovementCost.C + case "D": + return ImprovementCost.D + case undefined: + return undefined as Nullish + default: + return assertExhaustive(ic) } } diff --git a/src/shared/domain/adventurePoints/ratedEntry.ts b/src/shared/domain/adventurePoints/ratedEntry.ts index b7c0383a..fbfb8e0f 100644 --- a/src/shared/domain/adventurePoints/ratedEntry.ts +++ b/src/shared/domain/adventurePoints/ratedEntry.ts @@ -36,15 +36,12 @@ export type RatedAdventurePointsCache = { } const groupBoundAdventurePointsByRating = ( - boundAdventurePoints: BoundAdventurePoints[] + boundAdventurePoints: BoundAdventurePoints[], ): ReadonlyMap => - boundAdventurePoints.reduce( - (map, { rating: boundRating, adventurePoints }) => { - const key = boundRating ?? "activation" - return map.set(key, (map.get(key) ?? 0) + adventurePoints) - }, - new Map(), - ) + boundAdventurePoints.reduce((map, { rating: boundRating, adventurePoints }) => { + const key = boundRating ?? "activation" + return map.set(key, (map.get(key) ?? 0) + adventurePoints) + }, new Map()) const accumulateCache = ( startValue: number, @@ -59,9 +56,8 @@ const accumulateCache = ( const usedBoundForStep = Math.min(costForStep, acc.remainingApplicableBound) const usedGeneralForStep = costForStep - usedBoundForStep - const newRemainingBound = acc.remainingApplicableBound - - usedBoundForStep - + (boundByValue.get(currentValue) ?? 0) + const newRemainingBound = + acc.remainingApplicableBound - usedBoundForStep + (boundByValue.get(currentValue) ?? 0) return { usedGeneral: acc.usedGeneral + usedGeneralForStep, @@ -72,8 +68,8 @@ const accumulateCache = ( { usedGeneral: 0, usedBound: 0, - remainingApplicableBound: (boundByValue.get(initialApplicableBoundKey) ?? 0), - } + remainingApplicableBound: boundByValue.get(initialApplicableBoundKey) ?? 0, + }, ) return { @@ -98,8 +94,7 @@ export const cachedAdventurePoints = ( general: 0, bound: 0, } - } - else { + } else { const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints) return accumulateCache(minValue + 1, value, minValue, boundByValue, ic) } @@ -120,8 +115,7 @@ export const cachedAdventurePointsForActivatable = ( general: 0, bound: 0, } - } - else { + } else { const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints) return accumulateCache(0, value, "activation", boundByValue, ic) } diff --git a/src/shared/domain/attribute.ts b/src/shared/domain/attribute.ts index 1ef449e1..271fd602 100644 --- a/src/shared/domain/attribute.ts +++ b/src/shared/domain/attribute.ts @@ -1,58 +1,43 @@ import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel" import { Race } from "optolith-database-schema/types/Race" -import { CharacterState } from "../../main_window/slices/characterSlice.ts" -import { filterNonNullable } from "../utils/array.ts" +import { mapNullable } from "../utils/nullable.ts" +import { Energy, EnergyWithBuyBack } from "./energy.ts" import { AttributeIdentifier } from "./identifier.ts" -import { Rated } from "./ratedEntry.ts" +import { Dependency, Rated, flattenMinimumRestrictions } from "./ratedEntry.ts" + +export const getAttributeValue = (dynamic: Rated | undefined): number => dynamic?.value ?? 8 export const getAttributeMinimum = ( - characterDerivedCharacteristics: CharacterState["derivedCharacteristics"], - dynamic: Rated, + lifePoints: Energy, + arcaneEnergy: EnergyWithBuyBack, + karmaPoints: EnergyWithBuyBack, + dynamicAttribute: Rated, + singleHighestMagicalPrimaryAttributeId: number | undefined, + magicalPrimaryAttributeDependencies: Dependency[], + blessedPrimaryAttributeId: number | undefined, + blessedPrimaryAttributeDependencies: Dependency[], + filterApplyingDependencies: (dependencies: Dependency[]) => Dependency[], + getSkillCheckAttributeMinimum: (id: number) => number | undefined, ): number => { - // (wiki: StaticDataRecord) => - // (hero: HeroModelRecord) => - // (mblessed_primary_attr: Maybe>) => - // (mhighest_magical_primary_attr: Maybe>) => + const isConstitution = dynamicAttribute.id === AttributeIdentifier.Constitution + const isHighestMagicalPrimaryAttribute = + dynamicAttribute.id === singleHighestMagicalPrimaryAttributeId + const isBlessedPrimaryAttribute = dynamicAttribute.id === blessedPrimaryAttributeId - const isConstitution = dynamic.id === AttributeIdentifier.Constitution + const minimumValues: number[][] = [ + [8], + flattenMinimumRestrictions(filterApplyingDependencies(dynamicAttribute.dependencies)), + isConstitution ? [lifePoints.purchased] : [], + isHighestMagicalPrimaryAttribute + ? [arcaneEnergy.purchased, ...flattenMinimumRestrictions(magicalPrimaryAttributeDependencies)] + : [], + isBlessedPrimaryAttribute + ? [karmaPoints.purchased, ...flattenMinimumRestrictions(blessedPrimaryAttributeDependencies)] + : [], + mapNullable(getSkillCheckAttributeMinimum(dynamicAttribute.id), min => [min]) ?? [], + ] - // const isHighestMagicalPrimaryAttribute = - // Maybe.elem (AtDA.id (hero_entry)) (fmap (ACA_.id) (mhighest_magical_primary_attr)) - - // const isBlessedPrimaryAttribute = - // Maybe.elem (AtDA.id (hero_entry)) (fmap (ACA_.id) (mblessed_primary_attr)) - - // const blessedPrimaryAttributeDependencies = HA.blessedPrimaryAttributeDependencies (hero) - - // const magicalPrimaryAttributeDependencies = HA.magicalPrimaryAttributeDependencies (hero) - - const minimumValues = filterNonNullable([ - 8, - // ...flattenDependencies (wiki) (hero) (AtDA.dependencies (hero_entry)), - isConstitution ? characterDerivedCharacteristics.lifePoints.purchased : undefined, - // ...(isHighestMagicalPrimaryAttribute - // ? [ sel2 (added), ...magicalPrimaryAttributeDependencies.map (x => x.minValue) ] - // : []), - // ...(isBlessedPrimaryAttribute - // ? [ sel3 (added), ...blessedPrimaryAttributeDependencies.map (x => x.minValue) ] - // : []), - // fromMaybe (8) - // (getSkillCheckAttributeMinimum ( - // SDA.skills (wiki), - // SDA.combatTechniques (wiki), - // SDA.spells (wiki), - // SDA.liturgicalChants (wiki), - // HA.attributes (hero), - // HA.skills (hero), - // HA.combatTechniques (hero), - // HA.spells (hero), - // HA.liturgicalChants (hero), - // HA.skillCheckAttributeCache (hero), - // AtDA.id (hero_entry), - // )), - ]) - - return Math.max(...minimumValues) + return Math.max(...minimumValues.flat()) } /** @@ -60,16 +45,14 @@ export const getAttributeMinimum = ( * race `race` */ const getModIfSelectedAdjustment = (id: number, race: Race) => - race.attribute_adjustments - .find(adjustment => - adjustment.list.length > 1 - && adjustment.list.some(attribute => attribute.id.attribute === id)) - ?.value ?? 0 + race.attribute_adjustments.selectable?.find(adjustment => + adjustment.list.some(attribute => attribute.id.attribute === id), + )?.value ?? 0 const getModIfStaticAdjustment = (id: number, race: Race) => - race.attribute_adjustments - .filter(adjustment => adjustment.list.length === 1 && adjustment.list[0]!.id.attribute === id) - .reduce((acc, adjustment) => acc + adjustment.value, 0) + race.attribute_adjustments.fixed + ?.filter(adjustment => adjustment.id.attribute === id) + .reduce((acc, adjustment) => acc + adjustment.value, 0) ?? 0 export const getAttributeMaximum = ( isInCharacterCreation: boolean, @@ -94,10 +77,7 @@ export const getAttributeMaximum = ( return undefined } -export const isAttributeDecreasable = ( - dynamic: Rated, - min: number, -) => min < dynamic.value +export const isAttributeDecreasable = (dynamic: Rated, min: number) => min < dynamic.value export const isAttributeIncreasable = ( dynamic: Rated, @@ -106,5 +86,5 @@ export const isAttributeIncreasable = ( maxTotalPoints: number, isInCharacterCreation: boolean, ) => - (!isInCharacterCreation || totalPoints < maxTotalPoints) - && (max === undefined || dynamic.value < max) + (!isInCharacterCreation || totalPoints < maxTotalPoints) && + (max === undefined || dynamic.value < max) diff --git a/src/shared/domain/availability.ts b/src/shared/domain/availability.ts index 334ec2a5..61140312 100644 --- a/src/shared/domain/availability.ts +++ b/src/shared/domain/availability.ts @@ -9,9 +9,10 @@ export const isPublicationEnabled = ( publication: Publication, includeAllPublications: boolean, includePublications: number[], -) => publication.category === "CoreRules" - || (!publication.contains_adult_content && includeAllPublications) - || includePublications.includes(publication.id) +) => + publication.category === "CoreRules" || + (!publication.contains_adult_content && includeAllPublications) || + includePublications.includes(publication.id) /** * Checks if an entry from one or more publications is available, based on if @@ -25,6 +26,8 @@ export const isEntryAvailable = ( ) => sourceReferences.some(ref => { const publication = publications[ref.id.publication] - return publication !== undefined - && isPublicationEnabled(publication, includeAllPublications, includePublications) + return ( + publication !== undefined && + isPublicationEnabled(publication, includeAllPublications, includePublications) + ) }) diff --git a/src/shared/domain/combatTechnique.ts b/src/shared/domain/combatTechnique.ts new file mode 100644 index 00000000..f21aa0e1 --- /dev/null +++ b/src/shared/domain/combatTechnique.ts @@ -0,0 +1,132 @@ +import { CloseCombatTechnique } from "optolith-database-schema/types/CombatTechnique_Close" +import { RangedCombatTechnique } from "optolith-database-schema/types/CombatTechnique_Ranged" +import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel" +import { filterNonNullable } from "../utils/array.ts" +import { + Activatable, + PredefinedActivatableOption, + countOptions, + isActive, +} from "./activatableEntry.ts" +import { getAttributeValue } from "./attribute.ts" +import { AttributeIdentifier } from "./identifier.ts" +import { Dependency, Rated, RatedMap, flattenMinimumRestrictions } from "./ratedEntry.ts" + +export const getCombatTechniqueValue = (dynamic: Rated | undefined): number => dynamic?.value ?? 6 + +export type CombatTechnique = + | { tag: "CloseCombatTechnique"; closeCombatTechnique: CloseCombatTechnique } + | { tag: "RangedCombatTechnique"; rangedCombatTechnique: RangedCombatTechnique } + +const getPrimaryAttributeModifier = (dynamicAttributes: RatedMap, ids: number[]): number => { + const primaryAttributeValues = ids.map(id => getAttributeValue(dynamicAttributes[id])) + // return +1 for every 3 points above 8 + return Math.max(Math.floor((Math.max(...primaryAttributeValues) - 8) / 3), 0) +} + +const getAttackBase = ( + dynamicAttributes: RatedMap, + primaryAttributeIds: number[], + dynamicEntry: Rated | undefined, +): number => + getPrimaryAttributeModifier(dynamicAttributes, primaryAttributeIds) + + getCombatTechniqueValue(dynamicEntry) + +/** + * Returns the attack base for a close combat technique. + */ +export const getAttackBaseForClose = ( + dynamicAttributes: RatedMap, + dynamicEntry: Rated | undefined, +): number => getAttackBase(dynamicAttributes, [AttributeIdentifier.Courage], dynamicEntry) + +/** + * Returns the attack base for a ranged combat technique. + */ +export const getAttackBaseForRanged = ( + dynamicAttributes: RatedMap, + staticEntry: RangedCombatTechnique, + dynamicEntry: Rated | undefined, +): number => + getAttackBase( + dynamicAttributes, + staticEntry.primary_attribute.map(ref => ref.id.attribute), + dynamicEntry, + ) + +/** + * Returns the parry base for a close combat technique. + */ +export const getParryBaseForClose = ( + dynamicAttributes: RatedMap, + staticEntry: CloseCombatTechnique, + dynamicEntry: Rated | undefined, +): number | undefined => { + if (staticEntry.special.can_parry) { + const primaryAttributeModifier = getPrimaryAttributeModifier( + dynamicAttributes, + staticEntry.primary_attribute.map(ref => ref.id.attribute), + ) + const combatTechniqueRating = getCombatTechniqueValue(dynamicEntry) + return primaryAttributeModifier + Math.round(combatTechniqueRating / 2) + } + + return undefined +} + +export const getCombatTechniqueMinimum = ( + rangedCombatTechniques: RatedMap, + staticCombatTechnique: CombatTechnique, + dynamicCombatTechnique: Rated, + hunter: Activatable | undefined, + filterApplyingDependencies: (dependencies: Dependency[]) => Dependency[], +): number => { + const minimumValues: number[][] = [ + [6], + flattenMinimumRestrictions(filterApplyingDependencies(dynamicCombatTechnique.dependencies)), + isActive(hunter) && + staticCombatTechnique.tag === "RangedCombatTechnique" && + dynamicCombatTechnique.value >= 10 && + Object.values(rangedCombatTechniques) + .map(getCombatTechniqueValue) + .filter(value => value >= 10).length === 1 + ? [10] + : [], + ] + + return Math.max(...minimumValues.flat()) +} + +export const getCombatTechniqueMaximum = ( + attributes: RatedMap, + combatTechnique: CombatTechnique, + isInCharacterCreation: boolean, + startExperienceLevel: ExperienceLevel | undefined, + exceptionalSkill: Activatable | undefined, +): number => { + const idObject: PredefinedActivatableOption["id"] = + combatTechnique.tag === "CloseCombatTechnique" + ? { type: "CloseCombatTechnique", value: combatTechnique.closeCombatTechnique.id } + : { type: "RangedCombatTechnique", value: combatTechnique.rangedCombatTechnique.id } + + const primaryAttribute = + combatTechnique.tag === "CloseCombatTechnique" + ? combatTechnique.closeCombatTechnique.primary_attribute + : combatTechnique.rangedCombatTechnique.primary_attribute + + const maximumValues = filterNonNullable([ + Math.max(...primaryAttribute.map(ref => getAttributeValue(attributes[ref.id.attribute]))) + 2, + isInCharacterCreation && startExperienceLevel !== undefined + ? startExperienceLevel.max_combat_technique_rating + : undefined, + ]) + + const exceptionalSkillBonus = countOptions(exceptionalSkill, idObject) + + return Math.min(...maximumValues) + exceptionalSkillBonus +} + +export const isCombatTechniqueDecreasable = (dynamic: Rated, min: number, canRemove: boolean) => + min < dynamic.value && canRemove + +export const isCombatTechniqueIncreasable = (dynamic: Rated, max: number) => dynamic.value < max diff --git a/src/shared/domain/culture.ts b/src/shared/domain/culture.ts index c67fb287..eed5f76b 100644 --- a/src/shared/domain/culture.ts +++ b/src/shared/domain/culture.ts @@ -1,13 +1,8 @@ import { Culture } from "optolith-database-schema/types/Culture" import { TranslateMap } from "../utils/translate.ts" -export const getCulture = ( - cultures: Record, - id: number, -): Culture | undefined => cultures[id] +export const getCulture = (cultures: Record, id: number): Culture | undefined => + cultures[id] -export const getFullCultureName = ( - translateMap: TranslateMap, - culture: Culture, -): string => +export const getFullCultureName = (translateMap: TranslateMap, culture: Culture): string => translateMap(culture.translations)?.name ?? "" diff --git a/src/shared/domain/dependencies/filterApplyingDependencies.ts b/src/shared/domain/dependencies/filterApplyingDependencies.ts new file mode 100644 index 00000000..5794c230 --- /dev/null +++ b/src/shared/domain/dependencies/filterApplyingDependencies.ts @@ -0,0 +1,155 @@ +// import { fmap } from "../../../Data/Functor" +// import { elem, filter, find, foldl, isList, List, map, maximumNonNegative } from "../../../Data/List" +// import { bindF, Maybe, Nothing, or, sum } from "../../../Data/Maybe" +// import { gt, gte, inc } from "../../../Data/Num" +// import { isRecord, Record } from "../../../Data/Record" +// import { HeroModelRecord } from "../../Models/Hero/HeroModel" +// import { ValueBasedDependent } from "../../Models/Hero/heroTypeHelpers" +// import { SkillOptionalDependency } from "../../Models/Hero/SkillOptionalDependency" +// import { Advantage } from "../../Models/Wiki/Advantage" +// import { RequireActivatable } from "../../Models/Wiki/prerequisites/ActivatableRequirement" +// import { SocialPrerequisite } from "../../Models/Wiki/prerequisites/SocialPrerequisite" +// import { StaticDataRecord } from "../../Models/Wiki/WikiModel" +// import { AbilityRequirement, Activatable } from "../../Models/Wiki/wikiTypeHelpers" +// import { getHeroStateItem } from "../heroStateUtils" +// import { pipe } from "../pipe" +// import { flattenPrerequisites } from "../Prerequisites/flattenPrerequisites" +// import { isNumber } from "../typeCheckUtils" +// import { getWikiEntry } from "../WikiUtils" + +import { assertExhaustive } from "../../utils/typeSafety.ts" +import { getAttributeValue } from "../attribute.ts" +import { getCombatTechniqueValue } from "../combatTechnique.ts" +import { getLiturgicalChantValue } from "../liturgicalChant.ts" +import { + ActivatableRatedWithEnhancementsMap, + Dependency as RatedDependency, + RatedMap, + compareWithRestriction, +} from "../ratedEntry.ts" +import { getSkillValue } from "../skill.ts" +import { getSpellValue } from "../spell.ts" + +/** + * `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. + * @param wiki The full wiki. + * @param state The current hero. + * @param dependencies The list of dependencies to flatten. + */ +export const filterApplyingRatedDependencies = + (ratedMaps: { + attributes: RatedMap + skills: RatedMap + closeCombatTechniques: RatedMap + rangedCombatTechniques: RatedMap + spells: ActivatableRatedWithEnhancementsMap + rituals: ActivatableRatedWithEnhancementsMap + liturgicalChants: ActivatableRatedWithEnhancementsMap + ceremonies: ActivatableRatedWithEnhancementsMap + }) => + (dependencies: RatedDependency[]): RatedDependency[] => + dependencies.filter(dep => { + if (dep.otherTargets) { + return dep.otherTargets.some(target => { + switch (target.tag) { + case "Attribute": + return compareWithRestriction( + dep.value, + getAttributeValue(ratedMaps.attributes[target.attribute]), + ) + case "Skill": + return compareWithRestriction( + dep.value, + getSkillValue(ratedMaps.skills[target.skill]), + ) + case "CloseCombatTechnique": + return compareWithRestriction( + dep.value, + getCombatTechniqueValue( + ratedMaps.closeCombatTechniques[target.close_combat_technique], + ), + ) + case "RangedCombatTechnique": + return compareWithRestriction( + dep.value, + getCombatTechniqueValue( + ratedMaps.rangedCombatTechniques[target.ranged_combat_technique], + ), + ) + case "Spell": + return compareWithRestriction( + dep.value, + getSpellValue(ratedMaps.spells[target.spell]), + ) + case "Ritual": + return compareWithRestriction( + dep.value, + getSpellValue(ratedMaps.rituals[target.ritual]), + ) + case "LiturgicalChant": + return compareWithRestriction( + dep.value, + getLiturgicalChantValue(ratedMaps.liturgicalChants[target.liturgical_chant]), + ) + case "Ceremony": + return compareWithRestriction( + dep.value, + getLiturgicalChantValue(ratedMaps.ceremonies[target.ceremony]), + ) + default: + return assertExhaustive(target) + } + }) + } + + return true + }) + +// (wiki: StaticDataRecord) => +// (state: HeroModelRecord) => +// +// (dependencies: List>) => +// map, T> +// (e => isRecord (e) +// ? pipe ( +// getWikiEntry (wiki) as (id: string) => Maybe, +// bindF (pipe ( +// prerequisites, +// flattenPrerequisites (Nothing) (Nothing), +// find ((r): r is AbilityRequirement => +// r !== "RCP" +// && !SocialPrerequisite.is (r) +// && isList (id (r)) +// && elem (origin (e)) (id (r) as List)) +// )), +// fmap (pipe ( +// id as (r: AbilityRequirement) => List, +// foldl +// (acc => pipe ( +// getHeroStateItem (state) as (id: string) => Maybe, +// fmap (pipe (value, gte (value (e)))), +// or, +// x => x ? inc (acc) : acc +// )) +// (0), +// gt (1), +// x => x ? 0 : value (e) +// )), +// sum +// ) +// (origin (e)) as T +// : e) +// (dependencies) + +// /** +// * Filters the list of dependencies of `ActivatableSkillDependent`s and returns +// * the maximum. Minimum: `0`. +// */ +// export const filterAndMaximumNonNegative = pipe( +// filter(isNumber), +// maximumNonNegative, +// ) diff --git a/src/shared/domain/energy.ts b/src/shared/domain/energy.ts new file mode 100644 index 00000000..789b0a4d --- /dev/null +++ b/src/shared/domain/energy.ts @@ -0,0 +1,36 @@ +/** + * The instance of an energy entry where points can be purchased but also + * (permanently) spent. + */ +export type Energy = { + /** + * The number of points purchased. + */ + readonly purchased: number + + /** + * The number of points permanently lost. + */ + readonly permanentlyLost: number +} + +/** + * The instance of an energy entry where points can be purchased but also + * (permanently) spent and bought back. + */ +export type EnergyWithBuyBack = { + /** + * The number of points purchased. + */ + readonly purchased: number + + /** + * The number of points permanently lost. + */ + readonly permanentlyLost: number + + /** + * The number of permanently lost points that have been bought back. + */ + readonly permanentlyLostBoughtBack: number +} diff --git a/src/shared/domain/enhancement.ts b/src/shared/domain/enhancement.ts index a789998b..417e9b8f 100644 --- a/src/shared/domain/enhancement.ts +++ b/src/shared/domain/enhancement.ts @@ -2,21 +2,21 @@ import { ActivatableIdentifier } from "optolith-database-schema/types/_Identifie export type EnhancementDependency = | { - tag: "Internal" + tag: "Internal" - /** - * The depending enhancement. - */ - id: number - } + /** + * The depending enhancement. + */ + id: number + } | { - tag: "External" + tag: "External" - /** - * The depending activatable. - */ - id: ActivatableIdentifier - } + /** + * The depending activatable. + */ + id: ActivatableIdentifier + } export type Enhancement = { id: number diff --git a/src/shared/domain/experienceLevel.ts b/src/shared/domain/experienceLevel.ts index 6a72c2bc..d076cbe3 100644 --- a/src/shared/domain/experienceLevel.ts +++ b/src/shared/domain/experienceLevel.ts @@ -8,8 +8,6 @@ export const getCurrentExperienceLevel = ( .sort((a, b) => a.adventure_points - b.adventure_points) .reduce( (acc, experienceLevel) => - experienceLevel.adventure_points <= totalAdventurePoints - ? experienceLevel - : acc, - experienceLevels[0] + experienceLevel.adventure_points <= totalAdventurePoints ? experienceLevel : acc, + experienceLevels[0], ) diff --git a/src/shared/domain/identifier.ts b/src/shared/domain/identifier.ts index 074a239b..67a55e84 100644 --- a/src/shared/domain/identifier.ts +++ b/src/shared/domain/identifier.ts @@ -52,6 +52,68 @@ export type EnergyIdentifier = | DerivedCharacteristicIdentifier.ArcaneEnergy | DerivedCharacteristicIdentifier.KarmaPoints +export enum SkillIdentifier { + Flying = 1, + Gaukelei = 2, + Climbing = 3, + BodyControl = 4, + FeatOfStrength = 5, + Riding = 6, + Swimming = 7, + SelfControl = 8, + Singing = 9, + Perception = 10, + Dancing = 11, + Pickpocket = 12, + Stealth = 13, + Carousing = 14, + Persuasion = 15, + Seduction = 16, + Intimidation = 17, + Etiquette = 18, + Streetwise = 19, + Empathy = 20, + FastTalk = 21, + Disguise = 22, + Willpower = 23, + Tracking = 24, + Ropes = 25, + Fishing = 26, + Orienting = 27, + PlantLore = 28, + AnimalLore = 29, + Survival = 30, + Gambling = 31, + Geography = 32, + History = 33, + Religions = 34, + Warfare = 35, + MagicalLore = 36, + Mechanics = 37, + Math = 38, + Law = 39, + MythsAndLegends = 40, + SphereLore = 41, + Astronomy = 42, + Alchemy = 43, + Sailing = 44, + Driving = 45, + Commerce = 46, + TreatPoison = 47, + TreatDisease = 48, + TreatSoul = 49, + TreatWounds = 50, + Woodworking = 51, + PrepareFood = 52, + Leatherworking = 53, + ArtisticAbility = 54, + Metalworking = 55, + Music = 56, + PickLocks = 57, + Earthencraft = 58, + Clothworking = 59, +} + export enum AdvantageIdentifier { CustomAdvantage = 0, Aptitude = 4, // Begabung @@ -113,10 +175,20 @@ export enum DisadvantageIdentifier { WenigeVisionen = 73, } +export enum RangedCombatTechniqueIdentifier { + SpittingFire = 4, +} + export enum CombatSpecialAbilityIdentifier { CombatReflexes = 12, } +export enum GeneralSpecialAbilityIdentifier { + CraftInstruments = 17, + Hunter = 18, + FireEater = 53, +} + export enum MagicalSpecialAbilityIdentifier { GrosseMeditation = 12, } diff --git a/src/shared/domain/liturgicalChant.ts b/src/shared/domain/liturgicalChant.ts new file mode 100644 index 00000000..67bbd601 --- /dev/null +++ b/src/shared/domain/liturgicalChant.ts @@ -0,0 +1,5 @@ +import { ActivatableRated } from "./ratedEntry.ts" + +export const getLiturgicalChantValue = ( + dynamic: ActivatableRated | undefined, +): number | undefined => dynamic?.value diff --git a/src/shared/domain/pages.ts b/src/shared/domain/pages.ts index fb0abb8b..13a4905b 100644 --- a/src/shared/domain/pages.ts +++ b/src/shared/domain/pages.ts @@ -1,39 +1,41 @@ -import { Page, PageRange as RawPageRange, SimpleOccurrence } from "optolith-database-schema/types/source/_PublicationRef" +import { + Page, + PageRange as RawPageRange, + SimpleOccurrence, +} from "optolith-database-schema/types/source/_PublicationRef" import { range } from "../utils/array.ts" import { Compare } from "../utils/compare.ts" import { assertExhaustive } from "../utils/typeSafety.ts" export const comparePage: Compare = (a, b) => { switch (a.tag) { - case "InsideCoverFront": return b.tag === "InsideCoverFront" - ? 0 - : -1 - case "InsideCoverBack": return b.tag === "InsideCoverBack" - ? 0 - : 1 - case "Numbered": return b.tag === "Numbered" - ? a.numbered - b.numbered - : b.tag === "InsideCoverFront" - ? 1 - : -1 - default: return assertExhaustive(a) + case "InsideCoverFront": + return b.tag === "InsideCoverFront" ? 0 : -1 + case "InsideCoverBack": + return b.tag === "InsideCoverBack" ? 0 : 1 + case "Numbered": + return b.tag === "Numbered" ? a.numbered - b.numbered : b.tag === "InsideCoverFront" ? 1 : -1 + default: + return assertExhaustive(a) } } -export const equalsPage = (a: Page, b: Page): boolean => - comparePage(a, b) === 0 +export const equalsPage = (a: Page, b: Page): boolean => comparePage(a, b) === 0 export const succ = (page: Page): Page => { switch (page.tag) { - case "InsideCoverFront": return { tag: "Numbered", numbered: 1 } - case "InsideCoverBack": return { tag: "InsideCoverFront", inside_cover_front: {} } - case "Numbered": return { tag: "Numbered", numbered: page.numbered + 1 } - default: return assertExhaustive(page) + case "InsideCoverFront": + return { tag: "Numbered", numbered: 1 } + case "InsideCoverBack": + return { tag: "InsideCoverFront", inside_cover_front: {} } + case "Numbered": + return { tag: "Numbered", numbered: page.numbered + 1 } + default: + return assertExhaustive(page) } } -export const numberToPage = (number: number): Page => - ({ tag: "Numbered", numbered: number }) +export const numberToPage = (number: number): Page => ({ tag: "Numbered", numbered: number }) export type PageRange = { firstPage: Page @@ -42,51 +44,46 @@ export type PageRange = { export const numberRangeToPageRange = (numberRange: SimpleOccurrence): PageRange => numberRange.last_page === undefined - ? ({ firstPage: numberToPage(numberRange.first_page) }) - : ({ - firstPage: numberToPage(numberRange.first_page), - lastPage: numberToPage(numberRange.last_page), - }) + ? { firstPage: numberToPage(numberRange.first_page) } + : { + firstPage: numberToPage(numberRange.first_page), + lastPage: numberToPage(numberRange.last_page), + } export const fromRawPageRange = (pageRange: RawPageRange): PageRange => pageRange.last_page === undefined - ? ({ firstPage: pageRange.first_page }) - : ({ - firstPage: pageRange.first_page, - lastPage: pageRange.last_page, - }) + ? { firstPage: pageRange.first_page } + : { + firstPage: pageRange.first_page, + lastPage: pageRange.last_page, + } -export const normalizePageRanges = ( - ranges: PageRange[], -): PageRange[] => +export const normalizePageRanges = (ranges: PageRange[]): PageRange[] => ranges .flatMap(({ firstPage, lastPage = firstPage }): Page[] => { if (firstPage.tag === "Numbered" && lastPage.tag === "Numbered") { - return range(firstPage.numbered, lastPage.numbered) - .map(numbered => ({ tag: "Numbered", numbered })) - } - else { - return [ firstPage, lastPage ] + return range(firstPage.numbered, lastPage.numbered).map(numbered => ({ + tag: "Numbered", + numbered, + })) + } else { + return [firstPage, lastPage] } }) - .filter((page, i, pages) => - pages.findIndex(p => equalsPage(page, p)) === i) + .filter((page, i, pages) => pages.findIndex(p => equalsPage(page, p)) === i) .sort(comparePage) - .reduce( - (acc, page): PageRange[] => { - const lastRange = acc[acc.length - 1] + .reduce((acc, page): PageRange[] => { + const lastRange = acc[acc.length - 1] - if (lastRange === undefined) { - return [ { firstPage: page } ] - } + if (lastRange === undefined) { + return [{ firstPage: page }] + } - const { firstPage, lastPage = firstPage } = lastRange + const { firstPage, lastPage = firstPage } = lastRange - if (equalsPage(page, succ(lastPage))) { - return [ ...acc.slice(0, -1), { firstPage, lastPage: page } ] - } + if (equalsPage(page, succ(lastPage))) { + return [...acc.slice(0, -1), { firstPage, lastPage: page }] + } - return [ ...acc, { firstPage: page } ] - }, - [] - ) + return [...acc, { firstPage: page }] + }, []) diff --git a/src/shared/domain/profession.ts b/src/shared/domain/profession.ts index 245f17b7..dc0b7a9d 100644 --- a/src/shared/domain/profession.ts +++ b/src/shared/domain/profession.ts @@ -1,11 +1,18 @@ import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel" -import { Profession, ProfessionName, ProfessionPackage, ProfessionTranslation, ProfessionVariant, ProfessionVersion } from "optolith-database-schema/types/Profession" +import { + Profession, + ProfessionName, + ProfessionPackage, + ProfessionTranslation, + ProfessionVariant, + ProfessionVersion, +} from "optolith-database-schema/types/Profession" import { LocaleMap } from "optolith-database-schema/types/_LocaleMap" -import { Sex } from "../../main_window/slices/characterSlice.ts" import { Nullish, isNotNullish } from "../utils/nullable.ts" import { TranslateMap } from "../utils/translate.ts" import { assertExhaustive } from "../utils/typeSafety.ts" import { ProfessionIdentifier } from "./identifier.ts" +import { Sex } from "./sex.ts" export type ProfessionParts = { base: Profession @@ -23,30 +30,31 @@ export const getProfessionParts = ( const selectedProfession = professions[id] if (selectedProfession === undefined) { return undefined - } - else { + } else { const selectedInstance = selectedProfession.versions.find(x => x.tag === "Experienced" - ? x.experienced.id === instanceId - : x.by_experience_level.id === instanceId) + ? x.experienced.id === instanceId + : x.by_experience_level.id === instanceId, + ) if (selectedInstance === undefined) { return undefined - } - else { - const selectedTranslations = selectedInstance.tag === "Experienced" - ? selectedInstance.experienced.translations - : selectedInstance?.by_experience_level.translations + } else { + const selectedTranslations = + selectedInstance.tag === "Experienced" + ? selectedInstance.experienced.translations + : selectedInstance?.by_experience_level.translations - const selectedPackage = selectedInstance.tag === "Experienced" - ? selectedInstance.experienced.package - : selectedInstance.by_experience_level.packages_map - .find(x => x.experience_level_id === startExperienceLevel.id)?.package + const selectedPackage = + selectedInstance.tag === "Experienced" + ? selectedInstance.experienced.package + : selectedInstance.by_experience_level.packages_map.find( + x => x.experience_level_id === startExperienceLevel.id, + )?.package if (selectedPackage === undefined) { return undefined - } - else { + } else { return { base: selectedProfession, instance: selectedInstance, @@ -64,27 +72,29 @@ export const getProfessionVariant = ( ): ProfessionVariant | undefined => { if (variantId === undefined) { return undefined - } - else { + } else { return profession.package.variants?.list.find(x => x.id === variantId) } } export const professionNameToString = ( sex: Sex, - professionName: T + professionName: T, ): string | Nullish => { if (professionName === undefined || typeof professionName === "string") { return professionName as string | Nullish - } - else { + } else { switch (sex.type) { - case "Male": return professionName.male - case "Female": return professionName.female + case "Male": + return professionName.male + case "Female": + return professionName.female case "BalThani": case "Tsajana": - case "Custom": return professionName.default - default: return assertExhaustive(sex) + case "Custom": + return professionName.default + default: + return assertExhaustive(sex) } } } @@ -104,9 +114,8 @@ export const getFullProfessionNameParts = ( customName?: string, ): FullProfessionNameParts => { if (profession.base.id === ProfessionIdentifier.OwnProfession) { - const name = customName - ?? professionNameToString(sex, translateMap(profession.translations)?.name) - ?? "" + const name = + customName ?? professionNameToString(sex, translateMap(profession.translations)?.name) ?? "" return { name, @@ -114,15 +123,18 @@ export const getFullProfessionNameParts = ( } } - const professionName = - professionNameToString(sex, translateMap(profession.translations)?.name) - const professionSubName = - professionNameToString(sex, translateMap(profession.translations)?.specification) - const professionVariantName = - professionNameToString(sex, translateMap(professionVariant?.translations)?.name) + const professionName = professionNameToString(sex, translateMap(profession.translations)?.name) + const professionSubName = professionNameToString( + sex, + translateMap(profession.translations)?.specification, + ) + const professionVariantName = professionNameToString( + sex, + translateMap(professionVariant?.translations)?.name, + ) if (professionSubName !== undefined || professionVariantName !== undefined) { - const specifications = [ professionSubName, professionVariantName ] + const specifications = [professionSubName, professionVariantName] .filter(isNotNullish) .join(", ") @@ -132,8 +144,7 @@ export const getFullProfessionNameParts = ( variantName: professionVariantName, fullName: `${professionName ?? ""} (${specifications})`, } - } - else { + } else { return { name: professionName ?? "", fullName: professionName ?? "", diff --git a/src/shared/domain/race.ts b/src/shared/domain/race.ts index c5e34665..016aa160 100644 --- a/src/shared/domain/race.ts +++ b/src/shared/domain/race.ts @@ -2,28 +2,18 @@ import { Race, RaceVariant } from "optolith-database-schema/types/Race" import { mapNullableDefault } from "../utils/nullable.ts" import { TranslateMap } from "../utils/translate.ts" -export const getRace = ( - races: Record, - id: number, -): Race | undefined => races[id] +export const getRace = (races: Record, id: number): Race | undefined => races[id] -export const getRaceVariant = ( - race: Race, - id: number | undefined, -): RaceVariant | undefined => - id === undefined || race.variant_dependent.tag !== "HasVariants" - ? undefined - : race.variant_dependent.has_variants.find(variant => variant.id === id) +export const getRaceVariant = (race: Race, id: number | undefined): RaceVariant | undefined => + id === undefined ? undefined : race.variants.find(variant => variant.id === id) export const getFullRaceName = ( translateMap: TranslateMap, race: Race, raceVariant?: RaceVariant, ): string => - `${translateMap(race.translations)?.name ?? ""}${ - mapNullableDefault( - translateMap(raceVariant?.translations)?.name, - str => ` (${str})`, - "" - ) - }` + `${translateMap(race.translations)?.name ?? ""}${mapNullableDefault( + translateMap(raceVariant?.translations)?.name, + str => ` (${str})`, + "", + )}` diff --git a/src/shared/domain/ratedEntry.ts b/src/shared/domain/ratedEntry.ts index 10df5bb6..f13e573f 100644 --- a/src/shared/domain/ratedEntry.ts +++ b/src/shared/domain/ratedEntry.ts @@ -1,15 +1,22 @@ -import { ActivatableIdentifier, SkillWithEnhancementsIdentifier } from "optolith-database-schema/types/_IdentifierGroup" +import { + ActivatableIdentifier, + RatedIdentifier, + SkillWithEnhancementsIdentifier, +} from "optolith-database-schema/types/_IdentifierGroup" import { ImprovementCost } from "./adventurePoints/improvementCost.ts" -import { BoundAdventurePoints, RatedAdventurePointsCache, cachedAdventurePoints, cachedAdventurePointsForActivatable } from "./adventurePoints/ratedEntry.ts" +import { + BoundAdventurePoints, + RatedAdventurePointsCache, + cachedAdventurePoints, + cachedAdventurePointsForActivatable, +} from "./adventurePoints/ratedEntry.ts" import { Enhancement } from "./enhancement.ts" /** * A required value from a prerequisite. Can either require a minimum or a * maximum value. */ -export type ValueRestriction = - | MinimumValueRestriction - | MaximumValueRestriction +export type ValueRestriction = MinimumValueRestriction | MaximumValueRestriction export type MinimumValueRestriction = { readonly tag: "Minimum" @@ -27,6 +34,18 @@ export const isMinimumRestriction = (x: ValueRestriction): x is MinimumValueRest export const isMaximumRestriction = (x: ValueRestriction): x is MaximumValueRestriction => x.tag === "Maximum" +export const compareWithRestriction = ( + restriction: ValueRestriction, + value: number | undefined, +): boolean => + isMinimumRestriction(restriction) + ? value === undefined + ? false + : value >= restriction.minimum + : value === undefined + ? true + : value <= restriction.maximum + /** * Describes a dependency on a certain rated entry. */ @@ -40,7 +59,7 @@ export type Dependency = { * If the source prerequisite targets multiple entries, the other entries are * listed here. */ - readonly otherTargets?: ActivatableIdentifier | SkillWithEnhancementsIdentifier + readonly otherTargets?: RatedIdentifier[] /** * The required value. @@ -48,6 +67,18 @@ export type Dependency = { readonly value: ValueRestriction } +/** + * Flattens the minimum restrictions of a list of dependencies. + */ +export const flattenMinimumRestrictions = (dependencies: Dependency[]): number[] => + dependencies.flatMap(dep => (isMinimumRestriction(dep.value) ? [dep.value.minimum] : [])) + +/** + * Flattens the maximum restrictions of a list of dependencies. + */ +export const flattenMaximumRestrictions = (dependencies: Dependency[]): number[] => + dependencies.flatMap(dep => (isMaximumRestriction(dep.value) ? [dep.value.maximum] : [])) + /** * The current value. */ @@ -86,6 +117,10 @@ export type Rated = { readonly boundAdventurePoints: BoundAdventurePoints[] } +export type RatedMap = { + [id: number]: Rated +} + export type RatedHelpers = { /** * Creates a new entry with an initial value if active. The initial @@ -124,22 +159,18 @@ export const createRatedHelpers = (config: { const updateCachedAdventurePoints = (entry: Rated): Rated => ({ ...entry, - cachedAdventurePoints: - cachedAdventurePoints( - entry.value, - minValue, - entry.boundAdventurePoints, - getImprovementCost(entry.id), - ), + cachedAdventurePoints: cachedAdventurePoints( + entry.value, + minValue, + entry.boundAdventurePoints, + getImprovementCost(entry.id), + ), }) const create: RatedHelpers["create"] = ( id, value = minValue, - { - dependencies = [], - boundAdventurePoints = [], - } = {}, + { dependencies = [], boundAdventurePoints = [] } = {}, ) => updateCachedAdventurePoints({ id, @@ -205,6 +236,10 @@ export type ActivatableRated = { readonly boundAdventurePoints: BoundAdventurePoints[] } +export type ActivatableRatedMap = { + [id: number]: ActivatableRated +} + export type ActivatableRatedHelpers = { /** * Creates a new entry with an initial value if active. The initial @@ -246,21 +281,17 @@ export const createActivatableRatedHelpers = (config: { const updateCachedAdventurePoints = (entry: ActivatableRated): ActivatableRated => ({ ...entry, - cachedAdventurePoints: - cachedAdventurePointsForActivatable( - entry.value, - entry.boundAdventurePoints, - getImprovementCost(entry.id), - ), + cachedAdventurePoints: cachedAdventurePointsForActivatable( + entry.value, + entry.boundAdventurePoints, + getImprovementCost(entry.id), + ), }) const create: ActivatableRatedHelpers["create"] = ( id, value, - { - dependencies = [], - boundAdventurePoints = [], - } = {}, + { dependencies = [], boundAdventurePoints = [] } = {}, ) => updateCachedAdventurePoints({ id, @@ -330,3 +361,7 @@ export type ActivatableRatedWithEnhancements = { [id: number]: Enhancement } } + +export type ActivatableRatedWithEnhancementsMap = { + [id: number]: ActivatableRatedWithEnhancements +} diff --git a/src/shared/domain/sex.ts b/src/shared/domain/sex.ts new file mode 100644 index 00000000..44fd272c --- /dev/null +++ b/src/shared/domain/sex.ts @@ -0,0 +1,55 @@ +/** + * The character's sex. It does not have to be binary, although it always must be specified how to handle it in the context of binary sex prerequisites. You can also provide a custom sex with a custom name. + */ +export type Sex = BinarySex | NonBinarySex | CustomSex + +/** + * A binary sex option. + */ +export type BinarySex = { + type: "Male" | "Female" +} + +/** + * A non-binary sex option. + */ +export type NonBinarySex = { + type: "BalThani" | "Tsajana" + + /** + * Defines how a non-binary sex should be treated when checking prerequisites. + */ + binaryHandling: BinaryHandling +} + +/** + * A custom non-binary sex option. + */ +export type CustomSex = { + type: "Custom" + + /** + * The custom sex name. + */ + name: string + + /** + * Defines how a non-binary sex should be treated when checking prerequisites. + */ + binaryHandling: BinaryHandling +} + +/** + * Defines how a non-binary sex should be treated when checking prerequisites. + */ +export type BinaryHandling = { + /** + * Defines if the sex should be treated as male when checking prerequisites. + */ + asMale: boolean + + /** + * Defines if the sex should be treated as female when checking prerequisites. + */ + asFemale: boolean +} diff --git a/src/shared/domain/skill.ts b/src/shared/domain/skill.ts index d7ba1db5..18116b29 100644 --- a/src/shared/domain/skill.ts +++ b/src/shared/domain/skill.ts @@ -1,64 +1,53 @@ import { Culture } from "optolith-database-schema/types/Culture" import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel" import { Skill } from "optolith-database-schema/types/Skill" -import { Activatable, RatedMap } from "../../main_window/slices/characterSlice.ts" import { filterNonNullable } from "../utils/array.ts" -import { countOptions } from "./activatableEntry.ts" -import { Rated } from "./ratedEntry.ts" +import { mapNullable } from "../utils/nullable.ts" +import { Activatable, countOptions, isActive } from "./activatableEntry.ts" +import { SkillIdentifier } from "./identifier.ts" +import { Dependency, Rated, RatedMap, flattenMinimumRestrictions } from "./ratedEntry.ts" import { getSkillCheckValues } from "./skillCheck.ts" -// const getMinSRByCraftInstruments = (state : HeroModelRecord) => -// (entry : Record) : Maybe => { -// const id = SCA_.id (entry) -// const { CraftInstruments } = SpecialAbilityId +export const getSkillValue = (dynamic: Rated | undefined): number => dynamic?.value ?? 0 -// if ((id === SkillId.Woodworking || id === SkillId.Metalworking) -// && isMaybeActive (lookupF (HA.specialAbilities (state)) -// (CraftInstruments))) { -// // Sum of Woodworking and Metalworking must be at least 12. -// const MINIMUM_SUM = 12 +const getSkillMinimumByCraftInstruments = ( + skills: RatedMap, + dynamicSkill: Rated, + craftInstruments: Activatable | undefined, +) => { + if ( + (dynamicSkill.id === SkillIdentifier.Woodworking || + dynamicSkill.id === SkillIdentifier.Metalworking) && + isActive(craftInstruments) + ) { + // Sum of Woodworking and Metalworking must be at least 12. + const MINIMUM_SUM = 12 + const otherSkillId = + dynamicSkill.id === SkillIdentifier.Woodworking + ? SkillIdentifier.Metalworking + : SkillIdentifier.Woodworking + const otherSkillRating = getSkillValue(skills[otherSkillId]) + return MINIMUM_SUM - otherSkillRating + } -// const otherSkillId = id === SkillId.Woodworking -// ? SkillId.Metalworking -// : SkillId.Woodworking + return undefined +} -// const otherSkillRating = pipe_ ( -// state, -// HA.skills, -// lookup (otherSkillId), -// fmap (SDA.value), -// sum -// ) +export const getSkillMinimum = ( + skills: RatedMap, + dynamicSkill: Rated, + craftInstruments: Activatable | undefined, + filterApplyingDependencies: (dependencies: Dependency[]) => Dependency[], +): number => { + const minimumValues: number[][] = [ + [0], + flattenMinimumRestrictions(filterApplyingDependencies(dynamicSkill.dependencies)), + mapNullable(getSkillMinimumByCraftInstruments(skills, dynamicSkill, craftInstruments), min => [ + min, + ]) ?? [], + ] -// return Just (MINIMUM_SUM - otherSkillRating) -// } - -// return Nothing -// } - - -// /** -// * Check if the dependencies allow the passed skill to be decreased. -// */ -// const getMinSRByDeps = (staticData : StaticDataRecord) => -// (hero : HeroModelRecord) => -// (entry : Record) : Maybe => -// pipe_ ( -// entry, -// SCA_.dependencies, -// flattenDependencies (staticData) (hero), -// ensure (notNull), -// fmap (maximum) -// ) - -export const getSkillMinimum = (): number => { - const minimumValues = filterNonNullable([ - 0, - // TODO: getMinSRByDeps (staticData) (hero) (entry), - // TODO: getMinSRByCraftInstruments (hero) (entry) - ]) - - return Math.max(...minimumValues) + return Math.max(...minimumValues.flat()) } export const getSkillMaximum = ( @@ -80,24 +69,17 @@ export const getSkillMaximum = ( return Math.min(...maximumValues) + exceptionalSkillBonus } -export const isSkillDecreasable = ( - dynamic: Rated, - min: number, - canRemove: boolean, -) => min < dynamic.value && canRemove +export const isSkillDecreasable = (dynamic: Rated, min: number, canRemove: boolean) => + min < dynamic.value && canRemove -export const isSkillIncreasable = ( - dynamic: Rated, - max: number, -) => - dynamic.value < max +export const isSkillIncreasable = (dynamic: Rated, max: number) => dynamic.value < max export const getSkillCommonness = ( culture: Culture, - skill: Skill + skill: Skill, ): "common" | "uncommon" | undefined => culture.common_skills.some(({ id: { skill: id } }) => skill.id === id) - ? "common" - : culture.uncommon_skills?.some(({ id: { skill: id } }) => skill.id === id) ?? false - ? "uncommon" - : undefined + ? "common" + : culture.uncommon_skills?.some(({ id: { skill: id } }) => skill.id === id) ?? false + ? "uncommon" + : undefined diff --git a/src/shared/domain/skillCheck.ts b/src/shared/domain/skillCheck.ts index 81c5ca2e..167d0d8b 100644 --- a/src/shared/domain/skillCheck.ts +++ b/src/shared/domain/skillCheck.ts @@ -1,40 +1,44 @@ import { Attribute } from "optolith-database-schema/types/Attribute" import { SkillCheck } from "optolith-database-schema/types/_SkillCheck" -import { attributeValue } from "../../main_window/slices/attributesSlice.ts" -import { RatedMap } from "../../main_window/slices/characterSlice.ts" +import { getAttributeValue } from "./attribute.ts" +import { RatedMap } from "./ratedEntry.ts" type Triple = [T, T, T] const zipTriple = ( - [ a, b, c ]: Triple, - [ x, y, z ]: Triple, + [a, b, c]: Triple, + [x, y, z]: Triple, f: (x: T, y: U) => V, -): Triple => [ f(a, x), f(b, y), f(c, z) ] +): Triple => [f(a, x), f(b, y), f(c, z)] type SkillCheckAttributes = Triple type SkillCheckValues = Triple -type DisplayedSkillCheck = Triple<{ attribute: Attribute; value: number}> +type DisplayedSkillCheck = Triple<{ attribute: Attribute; value: number }> +/** + * Returns the static attributes of a skill check. + */ export const getSkillCheckAttributes = ( attributes: Record, check: SkillCheck, -): SkillCheckAttributes => - [ - attributes[check[0].id.attribute]!, - attributes[check[1].id.attribute]!, - attributes[check[2].id.attribute]!, - ] +): SkillCheckAttributes => [ + attributes[check[0].id.attribute]!, + attributes[check[1].id.attribute]!, + attributes[check[2].id.attribute]!, +] -export const getSkillCheckValues = ( - attributes: RatedMap, - check: SkillCheck, -): SkillCheckValues => - [ - attributeValue(attributes[check[0].id.attribute]), - attributeValue(attributes[check[1].id.attribute]), - attributeValue(attributes[check[2].id.attribute]), - ] +/** + * Returns the attribute values of a skill check. + */ +export const getSkillCheckValues = (attributes: RatedMap, check: SkillCheck): SkillCheckValues => [ + getAttributeValue(attributes[check[0].id.attribute]), + getAttributeValue(attributes[check[1].id.attribute]), + getAttributeValue(attributes[check[2].id.attribute]), +] +/** + * Returns the attributes of a skill check, paired with their values. + */ export const getDisplayedSkillCheck = ( staticAttributes: Record, dynamicAttributes: RatedMap, diff --git a/src/shared/domain/spell.ts b/src/shared/domain/spell.ts new file mode 100644 index 00000000..a11a3a17 --- /dev/null +++ b/src/shared/domain/spell.ts @@ -0,0 +1,4 @@ +import { ActivatableRated } from "./ratedEntry.ts" + +export const getSpellValue = (dynamic: ActivatableRated | undefined): number | undefined => + dynamic?.value diff --git a/tsconfig.json b/tsconfig.json index 2c510a69..10f38850 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, - "noUnusedParameters": false, + "noUnusedParameters": true, "outDir": "app/", "removeComments": true, "resolveJsonModule": true,