refactor: combat techniques, isolate domain logic, use prettier

This commit is contained in:
Lukas Obermann
2023-08-19 10:56:53 +02:00
parent 88d7552f49
commit d49d398a94
51 changed files with 2428 additions and 1981 deletions
+4 -165
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
arrowParens: avoid
printWidth: 100
semi: false
tabWidth: 2
trailingComma: all
+7 -1
View File
@@ -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"
},
}
-123
View File
@@ -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<string>) => {
if (isJust (id)) {
dispatch (AttributesActions.setAdjustmentId (fromJust (id)))
}
},
})
const connectAttributes =
connect<AttributesStateProps, AttributesDispatchProps, AttributesOwnProps, AppStateRecord> (
mapStateToProps,
mapDispatchToProps
)
export const AttributesContainer = connectAttributes (Attributes)
@@ -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)
-58
View File
@@ -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<Theme>) {
if (isJust (theme)) {
dispatch (ConfigActions.setTheme (fromJust (theme)))
}
},
switchEnableEditingHeroAfterCreationPhase () {
dispatch (ConfigActions.switchEnableEditingHeroAfterCreationPhase ())
},
async saveConfig () {
await dispatch (IOActions.requestConfigSave)
},
setLocale (id: Maybe<Locale>) {
dispatch (LocaleActions.setLocale (id))
},
setFallbackLocale (id: Maybe<Locale>) {
dispatch (FallbackLocaleActions.setFallbackLocale (id))
},
switchEnableAnimations () {
dispatch (ConfigActions.switchEnableAnimations ())
},
})
const connectSettings =
connect<SettingsStateProps, SettingsDispatchProps, SettingsOwnProps, AppStateRecord> (
mapStateToProps,
mapDispatchToProps
)
export const SettingsContainer = connectSettings (Settings)
-47
View File
@@ -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<SkillsStateProps, SkillsDispatchProps, SkillsOwnProps, AppStateRecord> (
mapStateToProps,
mapDispatchToProps
)
export const SkillsContainer = connectSkills (Skills)
@@ -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<CombatTechnique>) =>
(hero_entry: Record<SkillDependent>): 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<CombatTechnique>) =>
(hero_entry: Record<SkillDependent>): Maybe<number> => {
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<Record<CombatTechniqueWithAttackParryBase>> =>
pipe_ (
wiki_combat_techniques,
foldrWithKey ((id: string) => (wiki_entry: Record<CombatTechnique>) => {
const hero_entry =
findWithDefault (createSkillDependentWithValue6 (id))
(id)
(combatTechniques)
if (id === CombatTechniqueId.SpittingFire
&& maybe (true)
(pipe (ADA.active, fnull))
(lookup<string> (SpecialAbilityId.Feuerschlucker)
(special_abilities))) {
// If SF Feuerschlucker is not active, do not
// show CT Spitting Fire
return ident as
ident<List<Record<CombatTechniqueWithAttackParryBase>>>
}
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<Record<CTWAPB>>) => (hero: HeroModelRecord) => {
const hunter = lookup<string> (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))
)
@@ -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<string, Record<AttributeDependent>>) =>
mapMaybe (pipe (lookupF (attributes), fmap (ADA.value)))
export const getAttributeValueWithDefault = maybe (8) (ADA.value)
@@ -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<string, number> (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<CombatTechnique>) =>
pipe (
getCombatTechniqueRating,
add (getPrimaryAttributeMod (state)
(CTA.gr (wikiEntry) === CombatTechniqueGroupId.Ranged
? CTA.primary (wikiEntry)
: List (AttrId.Courage)))
)
export const getParry =
(state: HeroModelRecord) =>
(wikiEntry: Record<CombatTechnique>) =>
(maybeStateEntry: Maybe<Record<SkillDependent>>): Maybe<number> =>
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<CombatTechnique>) =>
(instance: Record<SkillDependent>): 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<string | number> (SkDA.id (instance))),
Maybe.elem<boolean> (true),
x => x ? 1 : 0
)
(exceptionalSkill)
return SkDA.value (instance) >= base_max + bonus
}
export const isDecreaseDisabled =
(staticData: StaticDataRecord) =>
(state: HeroModelRecord) =>
(wikiEntry: Record<CombatTechnique>) =>
(instance: Record<SkillDependent>) =>
(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))
}
@@ -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<Record<AttributeCombined>>
list: Maybe<List<Record<CombatTechniqueWithRequirements>>>
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<string>
}
const CTWRA_ = CombatTechniqueWithRequirementsA_
export const CombatTechniques: React.FC<CombatTechniquesProps> = props => {
const {
addPoint,
attributes,
list,
staticData,
isRemovingEnabled,
removePoint,
setSortOrder,
sortOrder,
filterText,
setFilterText,
} = props
const [ infoId, setInfoId ] = React.useState<Maybe<string>> (Nothing)
const showInfo = React.useCallback (
(id: string) => setInfoId (Just (id)),
[ setInfoId ]
)
return (
<Page id="combattechniques">
<Options>
<SearchField
staticData={staticData}
value={filterText}
onChange={setFilterText}
fullWidth
/>
<SortOptions
sortOrder={sortOrder}
sort={setSortOrder}
staticData={staticData}
options={List (SortNames.Name, SortNames.Group, SortNames.IC)}
/>
</Options>
<MainContent>
<ListHeader>
<ListHeaderTag className="name">
{translate (staticData) ("combattechniques.header.name")}
</ListHeaderTag>
<ListHeaderTag className="group">
{translate (staticData) ("combattechniques.header.group")}
</ListHeaderTag>
<ListHeaderTag
className="value"
hint={translate (staticData) ("combattechniques.header.combattechniquerating.tooltip")}
>
{translate (staticData) ("combattechniques.header.combattechniquerating")}
</ListHeaderTag>
<ListHeaderTag
className="ic"
hint={translate (staticData) ("combattechniques.header.improvementcost.tooltip")}
>
{translate (staticData) ("combattechniques.header.improvementcost")}
</ListHeaderTag>
<ListHeaderTag
className="primary"
hint={translate (staticData) ("combattechniques.header.primaryattribute.tooltip")}
>
{translate (staticData) ("combattechniques.header.primaryattribute")}
</ListHeaderTag>
<ListHeaderTag
className="at"
hint={translate (staticData) ("combattechniques.header.attack.tooltip")}
>
{translate (staticData) ("combattechniques.header.attack")}
</ListHeaderTag>
<ListHeaderTag
className="pa"
hint={translate (staticData) ("combattechniques.header.parry.tooltip")}
>
{translate (staticData) ("combattechniques.header.parry")}
</ListHeaderTag>
{isRemovingEnabled ? <ListHeaderTag className="btn-placeholder" /> : null}
<ListHeaderTag className="btn-placeholder" />
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll>
<ListView>
{pipe_ (
list,
bindF (ensure (notNull)),
maybe<JSX.Element | JSX.Element[]>
(<ListPlaceholder staticData={staticData} type="combatTechniques" noResults />)
(pipe (
map (
(x: Record<CombatTechniqueWithRequirements>) => (
<CombatTechniqueListItem
key={CTWRA_.id (x)}
attributes={attributes}
combatTechnique={x}
currentInfoId={infoId}
selectForInfo={showInfo}
staticData={staticData}
addPoint={addPoint}
removePoint={removePoint}
isRemovingEnabled={isRemovingEnabled}
/>
)
),
toArray
))
)}
</ListView>
</Scroll>
</MainContent>
<WikiInfoContainer currentId={infoId} />
</Page>
)
}
@@ -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<Record<AttributeCombined>>
combatTechnique: Record<CombatTechniqueWithRequirements>
currentInfoId: Maybe<string>
isRemovingEnabled: boolean
addPoint: (id: string) => void
removePoint: (id: string) => void
selectForInfo: (id: string) => void
}
export const CombatTechniqueListItem: React.FC<CombatTechniqueListItemProps> = 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 (
<SkillListItem
id={CTWRA_.id (ct)}
name={CTWRA_.name (ct)}
sr={CTWRA_.value (ct)}
ic={CTWRA_.ic (ct)}
checkDisabled
addPoint={addPoint}
addDisabled={!CTWRA.isIncreasable (ct)}
removePoint={removePoint}
removeDisabled={!isRemovingEnabled || !CTWRA.isDecreasable (ct)}
addValues={List (
{ className: primaryClassName, value: primary },
{ className: "at", value: CTWRA.at (ct) },
{ className: "atpa" },
{
className: "pa",
value: fromMaybe<string | number> (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}
/>
)
}
+2 -1
View File
@@ -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 <Skills />
case "combat_techniques": return null
case "combat_techniques": return <CombatTechniques />
case "special_abilities": return null
case "spells": return null
case "liturgical_chants": return null
@@ -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> = 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 (
<ListItem
insertTopMargin={insertTopMargin}
active={
inlineLibraryEntryId?.tag === "CloseCombatTechnique" &&
inlineLibraryEntryId.close_combat_technique === id
}
>
<ListItemName name={name} onClick={handleSelectForInfo} />
<ListItemSeparator />
<ListItemGroup text={translate("Close Combat")} />
<ListItemValues>
<SkillRating sr={sr} addPoint={addPoint} />
<SkillImprovementCost ic={ic} />
<SkillAdditionalValues
addValues={[
{ className: primaryClassName, value: primaryStr },
{ className: "at", value: at },
{ className: "atpa" },
{
className: "pa",
value: pa ?? "—",
},
]}
/>
</ListItemValues>
<SkillButtons
addDisabled={addDisabled}
ic={ic}
id={id}
removeDisabled={removeDisabled}
sr={sr}
addPoint={addPoint}
removePoint={canRemove ? removePoint : undefined}
selectForInfo={handleSelectForInfo}
/>
</ListItem>
)
}
const MemoCloseCombatTechniquesListItem = memo(CloseCombatTechniquesListItem)
export { MemoCloseCombatTechniquesListItem as CloseCombatTechniquesListItem }
@@ -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 (
<Page id="combat-techniques">
<Options>
<TextField value={filterText} onChange={setFilterText} hint={translate("Search")} />
<RadioButtonGroup
active={sortOrder}
label={translate("Sort By")}
array={[
{
name: translate("Name"),
value: CombatTechniquesSortOrder.Name,
},
{
name: translate("Group"),
value: CombatTechniquesSortOrder.Group,
},
{
name: translate("Improvement Cost"),
value: CombatTechniquesSortOrder.ImprovementCost,
},
]}
onClick={handleChangeSortOrder}
/>
</Options>
<Main>
<ListHeader>
<ListHeaderTag className="name">
{translate("combattechniques.header.name")}
</ListHeaderTag>
<ListHeaderTag className="group">
{translate("combattechniques.header.group")}
</ListHeaderTag>
<ListHeaderTag
className="value"
hint={translate("combattechniques.header.combattechniquerating.tooltip")}
>
{translate("combattechniques.header.combattechniquerating")}
</ListHeaderTag>
<ListHeaderTag
className="ic"
hint={translate("combattechniques.header.improvementcost.tooltip")}
>
{translate("combattechniques.header.improvementcost")}
</ListHeaderTag>
<ListHeaderTag
className="primary"
hint={translate("combattechniques.header.primaryattribute.tooltip")}
>
{translate("combattechniques.header.primaryattribute")}
</ListHeaderTag>
<ListHeaderTag className="at" hint={translate("combattechniques.header.attack.tooltip")}>
{translate("combattechniques.header.attack")}
</ListHeaderTag>
<ListHeaderTag className="pa" hint={translate("combattechniques.header.parry.tooltip")}>
{translate("combattechniques.header.parry")}
</ListHeaderTag>
{canRemove ? <ListHeaderTag className="btn-placeholder" /> : null}
<ListHeaderTag className="btn-placeholder" />
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll>
{list.length > 0 ? (
<List>
{list.map((x, i) => {
const translation = translateMap(x.static.translations)
if (x.kind === "close") {
return (
<CloseCombatTechniquesListItem
key={`close--${x.static.id}`}
insertTopMargin={isTopMarginNeeded(sortOrder, x, list[i - 1])}
id={x.static.id}
name={translation?.name ?? ""}
sr={x.dynamic.value}
primary={x.static.primary_attribute}
ic={fromRaw(x.static.improvement_cost)}
addDisabled={!x.isIncreasable}
removeDisabled={!x.isDecreasable}
at={x.attackBase}
pa={x.parryBase}
addPoint={handleAddCloseCombatTechniquePoint}
removePoint={handleRemoveCloseCombatTechniquePoint}
/>
)
}
return (
<RangedCombatTechniquesListItem
key={`ranged--${x.static.id}`}
insertTopMargin={isTopMarginNeeded(sortOrder, x, list[i - 1])}
id={x.static.id}
name={translation?.name ?? ""}
sr={x.dynamic.value}
primary={x.static.primary_attribute}
ic={fromRaw(x.static.improvement_cost)}
addDisabled={!x.isIncreasable}
removeDisabled={!x.isDecreasable}
at={x.attackBase}
addPoint={handleAddRangedCombatTechniquePoint}
removePoint={handleRemoveRangedCombatTechniquePoint}
/>
)
})}
</List>
) : (
<ListPlaceholder type="combatTechniques" message={translate("No Results")} />
)}
</Scroll>
</Main>
<InlineLibrary />
</Page>
)
}
@@ -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> = 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 (
<ListItem
insertTopMargin={insertTopMargin}
active={inlineLibraryEntryId?.tag === "RangedCombatTechnique" && inlineLibraryEntryId.ranged_combat_technique === id}
>
<ListItemName name={name} onClick={handleSelectForInfo} />
<ListItemSeparator />
<ListItemGroup text={translate("Ranged Combat")} />
<ListItemValues>
<SkillRating
sr={sr}
addPoint={addPoint}
/>
<SkillImprovementCost ic={ic} />
<SkillAdditionalValues
addValues={[
{ className: primaryClassName, value: primaryStr },
{ className: "at", value: at },
{ className: "atpa" },
{
className: "pa",
value: "—",
}
]}
/>
</ListItemValues>
<SkillButtons
addDisabled={addDisabled}
ic={ic}
id={id}
removeDisabled={removeDisabled}
sr={sr}
addPoint={addPoint}
removePoint={canRemove ? removePoint : undefined}
selectForInfo={handleSelectForInfo}
/>
</ListItem>
)
}
const MemoRangedCombatTechniquesListItem = memo(RangedCombatTechniquesListItem)
export { MemoRangedCombatTechniquesListItem as RangedCombatTechniquesListItem }
@@ -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> = 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 (
<ListItem
@@ -86,38 +91,23 @@ const SkillListItem: React.FC<Props> = props => {
unrecommended={untyp}
insertTopMargin={insertTopMargin}
active={inlineLibraryEntryId?.tag === "Skill" && inlineLibraryEntryId.skill === id}
>
>
<ListItemName name={name} onClick={handleSelectForInfo} />
<ListItemSeparator />
<SkillGroup
addText={addText}
group={group}
getGroupName={getGroupName}
/>
<SkillGroup addText={addText} group={group} getGroupName={getGroupName} />
<ListItemValues>
<SkillRating
isNotActive={isNotActive}
noIncrease={noIncrease}
sr={sr}
addPoint={addPoint}
/>
{checkDisabled !== true && check !== undefined
? (
<SkillCheck
check={check}
checkPenalty={checkmod}
/>
)
: null}
<SkillFill
addFillElement={addFillElement}
/>
<SkillImprovementCost
ic={ic}
/>
<SkillAdditionalValues
addValues={addValues}
/>
/>
{checkDisabled !== true && check !== undefined ? (
<SkillCheck check={check} checkPenalty={checkmod} />
) : null}
<SkillFill addFillElement={addFillElement} />
<SkillImprovementCost ic={ic} />
<SkillAdditionalValues addValues={addValues} />
</ListItemValues>
<SkillButtons
activateDisabled={activateDisabled}
@@ -131,7 +121,7 @@ const SkillListItem: React.FC<Props> = props => {
addPoint={addPoint}
removePoint={canRemove ? removePoint : undefined}
selectForInfo={handleSelectForInfo}
/>
/>
</ListItem>
)
}
@@ -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 (
<Page id="skills">
<Options>
<TextField
value={filterText}
onChange={setFilterText}
hint={translate("Search")}
/>
<TextField value={filterText} onChange={setFilterText} hint={translate("Search")} />
<RadioButtonGroup
active={sortOrder}
label={translate("Sort By")}
@@ -122,12 +134,9 @@ export const Skills: FC = () => {
},
]}
onClick={handleChangeSortOrder}
/>
/>
<Grid size="medium">
<Checkbox
checked={cultureRatingVisibility}
onClick={handlSwitchCultureRatingVisibility}
>
<Checkbox checked={cultureRatingVisibility} onClick={handlSwitchCultureRatingVisibility}>
{translate("skills.commonskills")}
</Checkbox>
{cultureRatingVisibility ? <RecommendedReference /> : null}
@@ -135,25 +144,13 @@ export const Skills: FC = () => {
</Options>
<Main>
<ListHeader>
<ListHeaderTag className="name">
{translate("skills.header.name")}
</ListHeaderTag>
<ListHeaderTag className="group">
{translate("skills.header.group")}
</ListHeaderTag>
<ListHeaderTag
className="value"
hint={translate("skills.header.skillrating.tooltip")}
>
<ListHeaderTag className="name">{translate("skills.header.name")}</ListHeaderTag>
<ListHeaderTag className="group">{translate("skills.header.group")}</ListHeaderTag>
<ListHeaderTag className="value" hint={translate("skills.header.skillrating.tooltip")}>
{translate("skills.header.skillrating")}
</ListHeaderTag>
<ListHeaderTag className="check">
{translate("skills.header.check")}
</ListHeaderTag>
<ListHeaderTag
className="ic"
hint={translate("skills.header.improvementcost.tooltip")}
>
<ListHeaderTag className="check">{translate("skills.header.check")}</ListHeaderTag>
<ListHeaderTag className="ic" hint={translate("skills.header.improvementcost.tooltip")}>
{translate("skills.header.improvementcost")}
</ListHeaderTag>
{canRemove ? <ListHeaderTag className="btn-placeholder" /> : null}
@@ -161,37 +158,35 @@ export const Skills: FC = () => {
<ListHeaderTag className="btn-placeholder" />
</ListHeader>
<Scroll>
{
list.length > 0
? (
<List>
{list.map((x, i) => {
const translation = translateMap(x.static.translations)
return (
<SkillListItem
key={x.static.id}
id={x.static.id}
typ={cultureRatingVisibility && x.commonness === "common"}
untyp={cultureRatingVisibility && x.commonness === "uncommon"}
name={translation?.name ?? ""}
sr={x.dynamic.value}
check={x.static.check}
ic={fromRaw(x.static.improvement_cost)}
addDisabled={!x.isIncreasable}
addPoint={handleAdd}
removeDisabled={!x.isDecreasable}
removePoint={handleRemove}
addFillElement
insertTopMargin={isTopMarginNeeded(sortOrder, x, list[i - 1])}
group={x.static.group.id.skill_group}
getGroupName={getGroupName}
/>
)
})}
</List>
)
: <ListPlaceholder type="skills" message={translate("No Results")} />
}
{list.length > 0 ? (
<List>
{list.map((x, i) => {
const translation = translateMap(x.static.translations)
return (
<SkillListItem
key={x.static.id}
id={x.static.id}
typ={cultureRatingVisibility && x.commonness === "common"}
untyp={cultureRatingVisibility && x.commonness === "uncommon"}
name={translation?.name ?? ""}
sr={x.dynamic.value}
check={x.static.check}
ic={fromRaw(x.static.improvement_cost)}
addDisabled={!x.isIncreasable}
addPoint={handleAdd}
removeDisabled={!x.isDecreasable}
removePoint={handleRemove}
addFillElement
insertTopMargin={isTopMarginNeeded(sortOrder, x, list[i - 1])}
group={x.static.group.id.skill_group}
getGroupName={getGroupName}
/>
)
})}
</List>
) : (
<ListPlaceholder type="skills" message={translate("No Results")} />
)}
</Scroll>
</Main>
<InlineLibrary />
@@ -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<SpentAdventurePoints>(
(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<SpentAdventurePoints>(
(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 (
+191 -133
View File
@@ -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<number, DisplayedPrimaryAttribute>
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<number, DisplayedPrimaryAttribute>; 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(
}
}
}
}
},
)
@@ -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<Record<CTWAPB>>) => (hero: HeroModelRecord) => {
// const hunter = lookup<string>(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),
// ),
// )
@@ -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),
)
@@ -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: [] },
)
+79 -36
View File
@@ -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),
}
})
},
)
+13 -163
View File
@@ -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<CharacterState, "dateCreated" | "dateLastModified"> = {
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<string>("character/setName")
export const setAvatar = createAction<string>("character/setAvatar")
@@ -900,4 +748,6 @@ export const characterReducer =
professionReducer,
rulesReducer,
skillsReducer,
closeCombatTechniquesReducer,
rangedCombatTechniquesReducer,
)
@@ -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,
}
@@ -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,
}
+39 -37
View File
@@ -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<N extends string, E extends string> = {
@@ -80,60 +83,59 @@ export const createRatedSlice = <N extends string, E extends string>(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<N, E>["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<N, E>["getValue"] = entry => entry?.value ?? config.minValue
const incrementAction = createAction<number, `${N}/increment${E}`>(`${config.namespace}/increment${config.entityName}`)
const decrementAction = createAction<number, `${N}/decrement${E}`>(`${config.namespace}/decrement${config.entityName}`)
const incrementAction = createAction<number, `${N}/increment${E}`>(
`${config.namespace}/increment${config.entityName}`,
)
const decrementAction = createAction<number, `${N}/decrement${E}`>(
`${config.namespace}/decrement${config.entityName}`,
)
const reducer = createImmerReducer(
(state: Draft<CharacterState>, 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 {
+88 -1
View File
@@ -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<PredefinedActivatableOption["id"]> = (a, b) =>
a.type === b.type && a.value === b.value
+4 -8
View File
@@ -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)
@@ -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 = <T extends RawImprovementCost | undefined>(
ic: T
ic: T,
): ImprovementCost | Nullish<T> => {
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<T>
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<T>
default:
return assertExhaustive(ic)
}
}
+11 -17
View File
@@ -36,15 +36,12 @@ export type RatedAdventurePointsCache = {
}
const groupBoundAdventurePointsByRating = (
boundAdventurePoints: BoundAdventurePoints[]
boundAdventurePoints: BoundAdventurePoints[],
): ReadonlyMap<number | "activation", number> =>
boundAdventurePoints.reduce(
(map, { rating: boundRating, adventurePoints }) => {
const key = boundRating ?? "activation"
return map.set(key, (map.get(key) ?? 0) + adventurePoints)
},
new Map<number | "activation", number>(),
)
boundAdventurePoints.reduce((map, { rating: boundRating, adventurePoints }) => {
const key = boundRating ?? "activation"
return map.set(key, (map.get(key) ?? 0) + adventurePoints)
}, new Map<number | "activation", number>())
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)
}
+41 -61
View File
@@ -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<Record<AttributeCombined>>) =>
// (mhighest_magical_primary_attr: Maybe<Record<AttributeCombined>>) =>
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)
+8 -5
View File
@@ -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)
)
})
+132
View File
@@ -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
+3 -8
View File
@@ -1,13 +1,8 @@
import { Culture } from "optolith-database-schema/types/Culture"
import { TranslateMap } from "../utils/translate.ts"
export const getCulture = (
cultures: Record<number, Culture>,
id: number,
): Culture | undefined => cultures[id]
export const getCulture = (cultures: Record<number, Culture>, 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 ?? ""
@@ -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) =>
// <T extends number | boolean>
// (dependencies: List<T | Record<SkillOptionalDependency>>) =>
// map<T | Record<SkillOptionalDependency>, T>
// (e => isRecord (e)
// ? pipe (
// getWikiEntry (wiki) as (id: string) => Maybe<Activatable>,
// 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<string>))
// )),
// fmap (pipe (
// id as (r: AbilityRequirement) => List<string>,
// foldl<string, number>
// (acc => pipe (
// getHeroStateItem (state) as (id: string) => Maybe<ValueBasedDependent>,
// 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<number | boolean, number>(isNumber),
// maximumNonNegative,
// )
+36
View File
@@ -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
}
+12 -12
View File
@@ -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
+2 -4
View File
@@ -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],
)
+72
View File
@@ -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,
}
+5
View File
@@ -0,0 +1,5 @@
import { ActivatableRated } from "./ratedEntry.ts"
export const getLiturgicalChantValue = (
dynamic: ActivatableRated | undefined,
): number | undefined => dynamic?.value
+52 -55
View File
@@ -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<Page> = (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<PageRange[]>(
(acc, page): PageRange[] => {
const lastRange = acc[acc.length - 1]
.reduce<PageRange[]>((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 }]
}, [])
+49 -38
View File
@@ -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 = <T extends ProfessionName | undefined>(
sex: Sex,
professionName: T
professionName: T,
): string | Nullish<T> => {
if (professionName === undefined || typeof professionName === "string") {
return professionName as string | Nullish<T>
}
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 ?? "",
+8 -18
View File
@@ -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<number, Race>,
id: number,
): Race | undefined => races[id]
export const getRace = (races: Record<number, Race>, 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})`,
"",
)}`
+62 -27
View File
@@ -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
}
+55
View File
@@ -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
}
+48 -66
View File
@@ -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<SkillCombined>) : Maybe<number> => {
// 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 <string> (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<SkillCombined>) : Maybe<number> =>
// 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
+25 -21
View File
@@ -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, T]
const zipTriple = <T, U, V>(
[ a, b, c ]: Triple<T>,
[ x, y, z ]: Triple<U>,
[a, b, c]: Triple<T>,
[x, y, z]: Triple<U>,
f: (x: T, y: U) => V,
): Triple<V> => [ f(a, x), f(b, y), f(c, z) ]
): Triple<V> => [f(a, x), f(b, y), f(c, z)]
type SkillCheckAttributes = Triple<Attribute>
type SkillCheckValues = Triple<number>
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<number, Attribute>,
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<number, Attribute>,
dynamicAttributes: RatedMap,
+4
View File
@@ -0,0 +1,4 @@
import { ActivatableRated } from "./ratedEntry.ts"
export const getSpellValue = (dynamic: ActivatableRated | undefined): number | undefined =>
dynamic?.value
+1 -1
View File
@@ -18,7 +18,7 @@
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"noUnusedParameters": true,
"outDir": "app/",
"removeComments": true,
"resolveJsonModule": true,