refactor: migrate attributes tab except attribute adjustment component
This commit is contained in:
+5
-2
@@ -91,7 +91,11 @@ rules:
|
||||
no-labels: 2
|
||||
no-lone-blocks: 2
|
||||
no-loop-func: 2
|
||||
no-multi-spaces: 2
|
||||
no-multi-spaces:
|
||||
- error
|
||||
- exceptions:
|
||||
Property: true
|
||||
SwitchCase: true
|
||||
no-multi-str: 2
|
||||
no-new: 2
|
||||
no-new-func: 2
|
||||
@@ -219,7 +223,6 @@ rules:
|
||||
- ["in", "instanceof"]
|
||||
allowSamePrecedence: true
|
||||
no-mixed-spaces-and-tabs: 2
|
||||
no-multi-assign: 2
|
||||
no-multiple-empty-lines: 2
|
||||
no-negated-condition: 2
|
||||
no-new-object: 2
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
import { equals } from "../../Data/Eq"
|
||||
import { blackbirdF } from "../../Data/Function"
|
||||
import { fmap, fmapF, mapReplace } from "../../Data/Functor"
|
||||
import { consF, elem, filter, find, flength, head, List, map, maximum, NonEmptyList, notNull } from "../../Data/List"
|
||||
import { any, bind, bindF, ensure, fromJust, fromMaybe, isJust, isNothing, join, Just, liftM2, mapMaybe, maybe, Maybe, Nothing, or } from "../../Data/Maybe"
|
||||
import { add, gte, lt, max, multiply, subtractBy } from "../../Data/Num"
|
||||
import { elems, foldr, lookup, lookupF } from "../../Data/OrderedMap"
|
||||
import { Record } from "../../Data/Record"
|
||||
import { fst, snd, Tuple } from "../../Data/Tuple"
|
||||
import { uncurryN, uncurryN3 } from "../../Data/Tuple/Curry"
|
||||
import { sel1, sel2, sel3 } from "../../Data/Tuple/Select"
|
||||
import { AttrId } from "../Constants/Ids"
|
||||
import { AttributeDependent, createPlainAttributeDependent } from "../Models/ActiveEntries/AttributeDependent"
|
||||
import { Energies } from "../Models/Hero/Energies"
|
||||
import { HeroModel, HeroModelRecord } from "../Models/Hero/HeroModel"
|
||||
import { AttributeCombined, AttributeCombinedA_ } from "../Models/View/AttributeCombined"
|
||||
import { AttributeWithRequirements } from "../Models/View/AttributeWithRequirements"
|
||||
import { Attribute } from "../Models/Wiki/Attribute"
|
||||
import { ExperienceLevel } from "../Models/Wiki/ExperienceLevel"
|
||||
import { Race } from "../Models/Wiki/Race"
|
||||
import { StaticData, StaticDataRecord } from "../Models/Wiki/WikiModel"
|
||||
import { createMaybeSelector } from "../Utilities/createMaybeSelector"
|
||||
import { flattenDependencies } from "../Utilities/Dependencies/flattenDependencies"
|
||||
import { getSkillCheckAttributeMinimum } from "../Utilities/Increasable/AttributeSkillCheckMinimum"
|
||||
import { pipe, pipe_ } from "../Utilities/pipe"
|
||||
import { mapTradHeroEntryToAttrCombined } from "../Utilities/primaryAttributeUtils"
|
||||
import { getCurrentEl, getStartEl } from "./elSelectors"
|
||||
import { getBlessedTraditionFromState } from "./liturgicalChantsSelectors"
|
||||
import { getRace } from "./raceSelectors"
|
||||
import { getMagicalTraditionsFromHero } from "./spellsSelectors"
|
||||
import { getAttributes, getAttributeValueLimit, getCurrentAttributeAdjustmentId, getCurrentHeroPresent, getCurrentPhase, getHeroProp, getWiki, getWikiAttributes } from "./stateSelectors"
|
||||
|
||||
const SDA = StaticData.A
|
||||
const HA = HeroModel.A
|
||||
const EA = Energies.A
|
||||
const ACA = AttributeCombined.A
|
||||
const ACA_ = AttributeCombinedA_
|
||||
const AA = Attribute.A
|
||||
const AtDA = AttributeDependent.A
|
||||
const AWRA = AttributeWithRequirements.A
|
||||
|
||||
export const getAttributeSum = createMaybeSelector (
|
||||
getAttributes,
|
||||
getWikiAttributes,
|
||||
uncurryN (hero_attrs => foldr (pipe (
|
||||
AA.id,
|
||||
lookupF (hero_attrs),
|
||||
maybe (add (8))
|
||||
(pipe (AtDA.value, add))
|
||||
))
|
||||
(0))
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the modifier if the attribute specified by `id` is a member of the
|
||||
* race `race`
|
||||
*/
|
||||
const getModIfSelectedAdjustment =
|
||||
(id: string) =>
|
||||
(race: Record<Race>) =>
|
||||
pipe_ (
|
||||
race,
|
||||
Race.A.attributeAdjustmentsSelection,
|
||||
snd,
|
||||
ensure (elem (id)),
|
||||
mapReplace (fst (Race.A.attributeAdjustmentsSelection (race))),
|
||||
Maybe.sum
|
||||
)
|
||||
|
||||
const getModIfStaticAdjustment =
|
||||
(id: string) =>
|
||||
pipe (
|
||||
Race.A.attributeAdjustments,
|
||||
List.lookup (id),
|
||||
Maybe.sum
|
||||
)
|
||||
|
||||
const getAttributeMaximum =
|
||||
(id: string) =>
|
||||
(mrace: Maybe<Record<Race>>) =>
|
||||
(adjustmentId: string) =>
|
||||
(startEl: Maybe<Record<ExperienceLevel>>) =>
|
||||
(currentEl: Maybe<Record<ExperienceLevel>>) =>
|
||||
(phase: Maybe<number>) =>
|
||||
(attributeValueLimit: Maybe<boolean>): Maybe<number> => {
|
||||
if (any (lt (3)) (phase)) {
|
||||
if (isJust (mrace)) {
|
||||
const race = fromJust (mrace)
|
||||
const selectedAdjustment = adjustmentId === id ? getModIfSelectedAdjustment (id) (race) : 0
|
||||
const staticAdjustment = getModIfStaticAdjustment (id) (race)
|
||||
|
||||
return fmapF (startEl)
|
||||
(pipe (
|
||||
ExperienceLevel.A.maxAttributeValue,
|
||||
add (selectedAdjustment + staticAdjustment)
|
||||
))
|
||||
}
|
||||
|
||||
return Just (0)
|
||||
}
|
||||
|
||||
if (or (attributeValueLimit)) {
|
||||
return fmapF (currentEl) (pipe (ExperienceLevel.A.maxAttributeValue, add (2)))
|
||||
}
|
||||
|
||||
return Nothing
|
||||
}
|
||||
|
||||
const getAttributeMinimum =
|
||||
(wiki: StaticDataRecord) =>
|
||||
(hero: HeroModelRecord) =>
|
||||
|
||||
/**
|
||||
* `(lp, ae, kp)`
|
||||
*/
|
||||
(added: Tuple<[number, number, number]>) =>
|
||||
(mblessed_primary_attr: Maybe<Record<AttributeCombined>>) =>
|
||||
(mhighest_magical_primary_attr: Maybe<Record<AttributeCombined>>) =>
|
||||
(hero_entry: Record<AttributeDependent>): number => {
|
||||
const isConstitution = AtDA.id (hero_entry) === AttrId.Constitution
|
||||
|
||||
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 = [
|
||||
...flattenDependencies (wiki) (hero) (AtDA.dependencies (hero_entry)),
|
||||
...(isConstitution ? [ sel1 (added) ] : []),
|
||||
...(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 minimumValues.maximum ()
|
||||
}
|
||||
|
||||
const getAddedEnergies = createMaybeSelector (
|
||||
getHeroProp,
|
||||
hero => Tuple (
|
||||
pipe_ (hero, HA.energies, EA.addedLifePoints),
|
||||
pipe_ (hero, HA.energies, EA.addedArcaneEnergyPoints),
|
||||
pipe_ (hero, HA.energies, EA.addedKarmaPoints)
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a `List` of attributes containing the current state and full wiki
|
||||
* info.
|
||||
*/
|
||||
export const getAttributesForSheet = createMaybeSelector (
|
||||
getAttributes,
|
||||
getWikiAttributes,
|
||||
uncurryN (hero_entries => pipe (
|
||||
elems,
|
||||
map (wiki_entry => {
|
||||
const id = AA.id (wiki_entry)
|
||||
|
||||
return AttributeCombined ({
|
||||
stateEntry: fromMaybe (createPlainAttributeDependent (id))
|
||||
(lookup (id) (hero_entries)),
|
||||
wikiEntry: wiki_entry,
|
||||
})
|
||||
})
|
||||
))
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the maximum attribute value of the list of given attribute ids.
|
||||
*/
|
||||
export const getMaxAttributeValueByID =
|
||||
(attributes: HeroModel["attributes"]) =>
|
||||
pipe (
|
||||
mapMaybe (pipe (lookupF (attributes), fmap (AtDA.value))),
|
||||
consF (8),
|
||||
maximum
|
||||
)
|
||||
|
||||
export const getPrimaryMagicalAttributes = createMaybeSelector (
|
||||
getWikiAttributes,
|
||||
getAttributes,
|
||||
getMagicalTraditionsFromHero,
|
||||
uncurryN3 (wiki_attributes =>
|
||||
hero_attributes =>
|
||||
mapMaybe (mapTradHeroEntryToAttrCombined (wiki_attributes) (hero_attributes)))
|
||||
)
|
||||
|
||||
export const getHighestPrimaryMagicalAttributeValue = createMaybeSelector (
|
||||
getPrimaryMagicalAttributes,
|
||||
pipe (ensure (notNull), fmap (List.foldr (pipe (ACA_.value, max)) (0)))
|
||||
)
|
||||
|
||||
export const getHighestPrimaryMagicalAttributes = createMaybeSelector (
|
||||
getPrimaryMagicalAttributes,
|
||||
getHighestPrimaryMagicalAttributeValue,
|
||||
uncurryN (attrs => fmap (max_value => filter (pipe (ACA_.value, equals (max_value))) (attrs)))
|
||||
)
|
||||
|
||||
type AttrCs = List<Record<AttributeCombined>>
|
||||
type NonEmptyAttrCs = NonEmptyList<Record<AttributeCombined>>
|
||||
|
||||
export const getHighestPrimaryMagicalAttribute = createMaybeSelector (
|
||||
getHighestPrimaryMagicalAttributes,
|
||||
pipe (
|
||||
bindF (ensure (pipe (flength, equals (1)) as (xs: AttrCs) => xs is NonEmptyAttrCs)),
|
||||
fmap ((xs: NonEmptyAttrCs) => head (xs))
|
||||
)
|
||||
)
|
||||
|
||||
export const getPrimaryMagicalAttributeForSheet = createMaybeSelector (
|
||||
getPrimaryMagicalAttributes,
|
||||
map (ACA_.short)
|
||||
)
|
||||
|
||||
export const getPrimaryBlessedAttribute = createMaybeSelector (
|
||||
getBlessedTraditionFromState,
|
||||
getAttributes,
|
||||
getWikiAttributes,
|
||||
(mtradition, hero_attributes, wiki_attributes) =>
|
||||
bind (mtradition) (mapTradHeroEntryToAttrCombined (wiki_attributes) (hero_attributes))
|
||||
)
|
||||
|
||||
export const getPrimaryBlessedAttributeForSheet = createMaybeSelector (
|
||||
getPrimaryBlessedAttribute,
|
||||
fmap (pipe (ACA.wikiEntry, AA.short))
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a `List` of attributes including state, full wiki infos and a
|
||||
* minimum and optional maximum value.
|
||||
*/
|
||||
export const getAttributesForView = createMaybeSelector (
|
||||
getCurrentHeroPresent,
|
||||
getStartEl,
|
||||
getCurrentEl,
|
||||
getCurrentPhase,
|
||||
getAttributeValueLimit,
|
||||
getWiki,
|
||||
getRace,
|
||||
getAddedEnergies,
|
||||
getPrimaryBlessedAttribute,
|
||||
getHighestPrimaryMagicalAttribute,
|
||||
(
|
||||
mhero,
|
||||
startEl,
|
||||
currentEl,
|
||||
mphase,
|
||||
attributeValueLimit,
|
||||
wiki,
|
||||
mrace,
|
||||
added,
|
||||
mblessed_primary_attr,
|
||||
mhighest_magical_primary_attr
|
||||
) =>
|
||||
fmapF (mhero)
|
||||
(hero => foldr ((wiki_entry: Record<Attribute>) => {
|
||||
const current_id = AA.id (wiki_entry)
|
||||
|
||||
const hero_entry = fromMaybe (createPlainAttributeDependent (current_id))
|
||||
(pipe_ (
|
||||
hero,
|
||||
HeroModel.A.attributes,
|
||||
lookup (current_id)
|
||||
))
|
||||
|
||||
const max_value =
|
||||
getAttributeMaximum (current_id)
|
||||
(mrace)
|
||||
(HeroModel.A.attributeAdjustmentSelected (hero))
|
||||
(startEl)
|
||||
(currentEl)
|
||||
(mphase)
|
||||
(attributeValueLimit)
|
||||
|
||||
const min_value =
|
||||
getAttributeMinimum (wiki)
|
||||
(hero)
|
||||
(added)
|
||||
(mblessed_primary_attr)
|
||||
(mhighest_magical_primary_attr)
|
||||
(hero_entry)
|
||||
|
||||
return consF (AttributeWithRequirements ({
|
||||
max: max_value,
|
||||
min: min_value,
|
||||
stateEntry: hero_entry,
|
||||
wikiEntry: wiki_entry,
|
||||
}))
|
||||
})
|
||||
(List.empty)
|
||||
(StaticData.A.attributes (wiki)))
|
||||
)
|
||||
|
||||
export const getCarryingCapacity = createMaybeSelector (
|
||||
getAttributes,
|
||||
pipe (lookup<string> (AttrId.Strength), maybe (8) (AtDA.value), multiply (2))
|
||||
)
|
||||
|
||||
export const getAdjustmentValue = createMaybeSelector (
|
||||
getRace,
|
||||
fmap (pipe (Race.A.attributeAdjustmentsSelection, fst))
|
||||
)
|
||||
|
||||
export const getCurrentAttributeAdjustment = createMaybeSelector (
|
||||
getCurrentAttributeAdjustmentId,
|
||||
getAttributesForView,
|
||||
uncurryN (blackbirdF (liftM2 ((id: string) => find (pipe (AWRA.wikiEntry, AA.id, equals (id)))))
|
||||
(join as join<Record<AttributeWithRequirements>>))
|
||||
)
|
||||
|
||||
export const getAvailableAdjustmentIds = createMaybeSelector (
|
||||
getRace,
|
||||
getAdjustmentValue,
|
||||
getAttributesForView,
|
||||
getCurrentAttributeAdjustment,
|
||||
(mrace, madjustmentValue, mattrsCalculated, mcurr_attr) =>
|
||||
fmapF (mrace)
|
||||
(pipe (
|
||||
Race.A.attributeAdjustmentsSelection,
|
||||
snd,
|
||||
adjustmentIds => {
|
||||
if (isJust (mcurr_attr)) {
|
||||
const curr_attr = fromJust (mcurr_attr)
|
||||
|
||||
const curr_attr_val = pipe_ (curr_attr, AWRA.stateEntry, AtDA.value)
|
||||
|
||||
if (or (pipe_ (curr_attr, AWRA.max, liftM2 (blackbirdF (subtractBy)
|
||||
(lt (curr_attr_val)))
|
||||
(madjustmentValue)))) {
|
||||
const curr_attr_id = pipe_ (curr_attr, AWRA.stateEntry, AtDA.id)
|
||||
|
||||
return List (curr_attr_id)
|
||||
}
|
||||
}
|
||||
|
||||
return filter ((id: string) => {
|
||||
const mattr = bind (mattrsCalculated)
|
||||
(find (pipe (AWRA.wikiEntry, AA.id, equals (id))))
|
||||
|
||||
if (isJust (mattr)) {
|
||||
const attr = fromJust (mattr)
|
||||
|
||||
const mmax = AWRA.max (attr)
|
||||
|
||||
const mcurr_attr_id = fmapF (mcurr_attr)
|
||||
(pipe (AWRA.stateEntry, AtDA.id))
|
||||
|
||||
if (isNothing (mmax) || Maybe.elem (id) (mcurr_attr_id)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isJust (madjustmentValue)) {
|
||||
const attr_val = pipe_ (attr, AWRA.stateEntry, AtDA.value)
|
||||
|
||||
return maybe (true)
|
||||
(pipe (
|
||||
add (fromJust (madjustmentValue)),
|
||||
gte (attr_val)
|
||||
))
|
||||
(mmax)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
(adjustmentIds)
|
||||
}
|
||||
))
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
import { fmap } from "../../Data/Functor"
|
||||
import { bind } from "../../Data/Maybe"
|
||||
import { lookupF } from "../../Data/OrderedMap"
|
||||
import { ExperienceLevel } from "../Models/Wiki/ExperienceLevel"
|
||||
import { createMaybeSelector } from "../Utilities/createMaybeSelector"
|
||||
import { getExperienceLevelIdByAp } from "../Utilities/ELUtils"
|
||||
import { pipe } from "../Utilities/pipe"
|
||||
import { getExperienceLevelStartId, getTotalAdventurePoints, getWikiExperienceLevels } from "./stateSelectors"
|
||||
|
||||
export const getCurrentEl = createMaybeSelector (
|
||||
getWikiExperienceLevels,
|
||||
getTotalAdventurePoints,
|
||||
(all_els, mtotal_ap) => bind (mtotal_ap)
|
||||
(pipe (getExperienceLevelIdByAp (all_els), lookupF (all_els)))
|
||||
)
|
||||
|
||||
export const getStartEl = createMaybeSelector (
|
||||
getWikiExperienceLevels,
|
||||
getExperienceLevelStartId,
|
||||
(all_els, mid) => bind (mid) (lookupF (all_els))
|
||||
)
|
||||
|
||||
export const getMaxTotalAttributeValues = createMaybeSelector (
|
||||
getStartEl,
|
||||
fmap (ExperienceLevel.A.maxTotalAttributeValues)
|
||||
)
|
||||
@@ -1,213 +0,0 @@
|
||||
#attribute {
|
||||
.scroll-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.counter {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main, .calculated, .permanent {
|
||||
display: flex;
|
||||
|
||||
.number-box {
|
||||
top: 70px;
|
||||
right: -6px;
|
||||
width: 24px;
|
||||
z-index: 2;
|
||||
|
||||
+ .btn-round {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
justify-content: center;
|
||||
padding-bottom: 16px;
|
||||
// border-bottom: 1px solid transparentize(white, .925);
|
||||
}
|
||||
|
||||
.calculated {
|
||||
.value {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.number-box {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.short {
|
||||
font: bold 14px/28px Alegreya;
|
||||
color: var(--headings-color);
|
||||
letter-spacing: 0.05em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.attr .value {
|
||||
border: 2px solid transparent;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.attribute-adjustment {
|
||||
margin: 10px 0 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> span::after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
> .dropdown {
|
||||
margin: 0 0 0 16px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.value-inner {
|
||||
border: 1px solid var(--separator-color-transparent);
|
||||
padding: 1px;
|
||||
|
||||
> div {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
font: bold 28px/45px Alegreya Sans;
|
||||
font-variant-numeric: lining-nums;
|
||||
color: var(--headings-color);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-round {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.attr {
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
position: relative;
|
||||
|
||||
&.ATTR_1 > .value {
|
||||
border-color: var(--courage-color);
|
||||
}
|
||||
|
||||
&.ATTR_2 > .value {
|
||||
border-color: var(--sagacity-color);
|
||||
}
|
||||
|
||||
&.ATTR_3 > .value {
|
||||
border-color: var(--intuition-color);
|
||||
}
|
||||
|
||||
&.ATTR_4 > .value {
|
||||
border-color: var(--charisma-color);
|
||||
}
|
||||
|
||||
&.ATTR_5 > .value {
|
||||
border-color: var(--dexterity-color);
|
||||
}
|
||||
|
||||
&.ATTR_6 > .value {
|
||||
border-color: var(--agility-color);
|
||||
}
|
||||
|
||||
&.ATTR_7 > .value {
|
||||
border-color: var(--constitution-color);
|
||||
}
|
||||
|
||||
&.ATTR_8 > .value {
|
||||
border-color: var(--strength-color);
|
||||
}
|
||||
}
|
||||
|
||||
.permanent {
|
||||
.value-inner > div {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.number-box {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 77px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay > .calc-attr-overlay {
|
||||
width: 280px;
|
||||
|
||||
p.calc-text {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.permanent-points-editor {
|
||||
.modal-header {
|
||||
padding: 16px 0;
|
||||
|
||||
.modal-header-inner {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1 1 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-bottom: 10px;
|
||||
|
||||
&:nth-child(2) {
|
||||
margin-left: 1px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: var(--separator-color-transparent);
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font: bold 40px/1 Alegreya;
|
||||
color: var(--headings-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
|
||||
.remove {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { List, map, toArray } from "../../../Data/List"
|
||||
import { fst } from "../../../Data/Tuple"
|
||||
import { DerivedCharacteristic } from "../../Models/Wiki/DerivedCharacteristic"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { DCPair } from "../../Selectors/derivedCharacteristicsSelectors"
|
||||
import { pipe_ } from "../../Utilities/pipe"
|
||||
import { AttributeCalcItem } from "./AttributeCalcItem"
|
||||
|
||||
export interface AttributesCalcProps {
|
||||
derived: List<DCPair>
|
||||
staticData: StaticDataRecord
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
addLifePoint (): void
|
||||
addArcaneEnergyPoint (): void
|
||||
addKarmaPoint (): void
|
||||
removeLifePoint (): void
|
||||
removeArcaneEnergyPoint (): void
|
||||
removeKarmaPoint (): void
|
||||
}
|
||||
|
||||
export const AttributeCalc: React.FC<AttributesCalcProps> = props => {
|
||||
const {
|
||||
derived,
|
||||
staticData,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
addLifePoint,
|
||||
addArcaneEnergyPoint,
|
||||
addKarmaPoint,
|
||||
removeLifePoint,
|
||||
removeArcaneEnergyPoint,
|
||||
removeKarmaPoint,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="calculated">
|
||||
{pipe_ (
|
||||
derived,
|
||||
map (attribute => (
|
||||
<AttributeCalcItem
|
||||
key={DerivedCharacteristic.A.id (fst (attribute))}
|
||||
attribute={attribute}
|
||||
staticData={staticData}
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
addLifePoint={addLifePoint}
|
||||
addArcaneEnergyPoint={addArcaneEnergyPoint}
|
||||
addKarmaPoint={addKarmaPoint}
|
||||
removeLifePoint={removeLifePoint}
|
||||
removeArcaneEnergyPoint={removeArcaneEnergyPoint}
|
||||
removeKarmaPoint={removeKarmaPoint}
|
||||
/>
|
||||
)),
|
||||
toArray
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { fmapF } from "../../../Data/Functor"
|
||||
import { bindF, ensure, fromJust, fromMaybe, isJust, liftM2, maybe, Maybe, or } from "../../../Data/Maybe"
|
||||
import { gt, subtractBy } from "../../../Data/Num"
|
||||
import { fst, snd } from "../../../Data/Tuple"
|
||||
import { DerivedCharacteristicValues } from "../../Models/View/DerivedCharacteristicCombined"
|
||||
import { DerivedCharacteristic } from "../../Models/Wiki/DerivedCharacteristic"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { DCPair } from "../../Selectors/derivedCharacteristicsSelectors"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { sign, signNeg } from "../../Utilities/NumberUtils"
|
||||
import { pipe, pipe_ } from "../../Utilities/pipe"
|
||||
import { IconButton } from "../Universal/IconButton"
|
||||
import { NumberBox } from "../Universal/NumberBox"
|
||||
import { AttributeBorder } from "./AttributeBorder"
|
||||
|
||||
export interface AttributeCalcItemProps {
|
||||
attribute: DCPair
|
||||
staticData: StaticDataRecord
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
addLifePoint (): void
|
||||
addArcaneEnergyPoint (): void
|
||||
addKarmaPoint (): void
|
||||
removeLifePoint (): void
|
||||
removeArcaneEnergyPoint (): void
|
||||
removeKarmaPoint (): void
|
||||
}
|
||||
|
||||
const DCA = DerivedCharacteristic.A
|
||||
const DCVA = DerivedCharacteristicValues.A
|
||||
|
||||
export const AttributeCalcItem: React.FC<AttributeCalcItemProps> = props => {
|
||||
const {
|
||||
attribute,
|
||||
staticData,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
addLifePoint,
|
||||
addArcaneEnergyPoint,
|
||||
addKarmaPoint,
|
||||
removeLifePoint,
|
||||
removeArcaneEnergyPoint,
|
||||
removeKarmaPoint,
|
||||
} = props
|
||||
|
||||
const id = DCA.id (fst (attribute))
|
||||
|
||||
const handleAddMaxEnergyPoint = React.useCallback (
|
||||
() => {
|
||||
switch (id) {
|
||||
case "LP":
|
||||
addLifePoint ()
|
||||
break
|
||||
|
||||
case "AE":
|
||||
addArcaneEnergyPoint ()
|
||||
break
|
||||
|
||||
case "KP":
|
||||
addKarmaPoint ()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[ addArcaneEnergyPoint, addKarmaPoint, addLifePoint, id ]
|
||||
)
|
||||
|
||||
const handleRemoveMaxEnergyPoint = React.useCallback (
|
||||
() => {
|
||||
switch (id) {
|
||||
case "LP":
|
||||
removeLifePoint ()
|
||||
break
|
||||
|
||||
case "AE":
|
||||
removeArcaneEnergyPoint ()
|
||||
break
|
||||
|
||||
case "KP":
|
||||
removeKarmaPoint ()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[ removeArcaneEnergyPoint, removeKarmaPoint, removeLifePoint, id ]
|
||||
)
|
||||
|
||||
const base = DCVA.base (snd (attribute))
|
||||
const mod = DCVA.mod (snd (attribute))
|
||||
const mcurrent_add = DCVA.currentAdd (snd (attribute))
|
||||
const mmax_add = DCVA.maxAdd (snd (attribute))
|
||||
const has_value = isJust (DCVA.value (snd (attribute)))
|
||||
const value = maybe ("\u2013") (signNeg) (DCVA.value (snd (attribute)))
|
||||
const mpermanent_lost = DCVA.permanentLost (snd (attribute))
|
||||
const mpermanent_redeemed = DCVA.permanentRedeemed (snd (attribute))
|
||||
|
||||
return (
|
||||
<AttributeBorder
|
||||
label={DCA.short (fst (attribute))}
|
||||
value={value}
|
||||
tooltip={(
|
||||
<div className="calc-attr-overlay">
|
||||
<h4>
|
||||
<span>{DCA.name (fst (attribute))}</span>
|
||||
<span>{value}</span>
|
||||
</h4>
|
||||
<p className="calc-text">
|
||||
{fromMaybe (DCA.calc (fst (attribute)))
|
||||
(DCVA.calc (snd (attribute)))}
|
||||
{" = "}
|
||||
{fromMaybe<string | number> ("\u2013") (base)}
|
||||
</p>
|
||||
{isJust (mod) || (isJust (mcurrent_add) && !isInCharacterCreation)
|
||||
? (
|
||||
<p>
|
||||
{isJust (mod)
|
||||
? (
|
||||
<span className="mod">
|
||||
{translate (staticData)
|
||||
("attributes.derivedcharacteristics.tooltips.modifier")}
|
||||
{": "}
|
||||
{sign (fromJust (mod))}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isJust (mcurrent_add) && !isInCharacterCreation
|
||||
? (
|
||||
<span className="add">
|
||||
{translate (staticData)
|
||||
("attributes.derivedcharacteristics.tooltips.bought")}
|
||||
{": "}
|
||||
{fromJust (mcurrent_add)}
|
||||
{" / "}
|
||||
{fromMaybe<string | number> ("\u2013") (mmax_add)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</p>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
tooltipMargin={7}
|
||||
>
|
||||
{pipe_ (
|
||||
mmax_add,
|
||||
bindF (ensure (maxAdd => !isInCharacterCreation && maxAdd > 0)),
|
||||
maybe (<></>) (maxAdd => (<NumberBox current={mcurrent_add} max={maxAdd} />))
|
||||
)}
|
||||
{
|
||||
has_value
|
||||
&& !isInCharacterCreation
|
||||
? fromMaybe (<></>)
|
||||
(liftM2 ((current_add: number) => (max_add: number) => (
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
onClick={handleAddMaxEnergyPoint}
|
||||
disabled={
|
||||
current_add >= max_add
|
||||
|| (
|
||||
id !== "LP"
|
||||
&& or (fmapF (mpermanent_lost)
|
||||
(pipe (
|
||||
subtractBy (Maybe.sum (mpermanent_redeemed)),
|
||||
gt (0)
|
||||
)))
|
||||
)
|
||||
}
|
||||
/>
|
||||
))
|
||||
(mcurrent_add)
|
||||
(mmax_add))
|
||||
: null
|
||||
}
|
||||
{
|
||||
has_value
|
||||
&& !isInCharacterCreation
|
||||
&& isRemovingEnabled
|
||||
&& isJust (mmax_add)
|
||||
? maybe (<></>)
|
||||
((current_add: number) => (
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
onClick={handleRemoveMaxEnergyPoint}
|
||||
disabled={
|
||||
current_add <= 0
|
||||
|| (
|
||||
id !== "LP"
|
||||
&& or (fmapF (mpermanent_lost)
|
||||
(pipe (
|
||||
subtractBy (Maybe.sum (mpermanent_redeemed)),
|
||||
gt (0)
|
||||
)))
|
||||
)
|
||||
}
|
||||
/>
|
||||
))
|
||||
(mcurrent_add)
|
||||
: null
|
||||
}
|
||||
</AttributeBorder>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { fmap } from "../../../Data/Functor"
|
||||
import { List, map, toArray } from "../../../Data/List"
|
||||
import { Maybe, maybe } from "../../../Data/Maybe"
|
||||
import { Record } from "../../../Data/Record"
|
||||
import { AttributeWithRequirements } from "../../Models/View/AttributeWithRequirements"
|
||||
import { Attribute } from "../../Models/Wiki/Attribute"
|
||||
import { pipe_ } from "../../Utilities/pipe"
|
||||
import { AttributeListItem } from "./AttributeListItem"
|
||||
|
||||
export interface AttributeListProps {
|
||||
attributes: Maybe<List<Record<AttributeWithRequirements>>>
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
maxTotalAttributeValues: Maybe<number>
|
||||
sum: number
|
||||
addPoint (id: string): void
|
||||
removePoint (id: string): void
|
||||
}
|
||||
|
||||
export const AttributeList: React.FC<AttributeListProps> = props => {
|
||||
const {
|
||||
attributes,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
maxTotalAttributeValues,
|
||||
sum,
|
||||
addPoint,
|
||||
removePoint,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="main">
|
||||
{pipe_ (
|
||||
attributes,
|
||||
fmap (map (
|
||||
(attr: Record<AttributeWithRequirements>) => (
|
||||
<AttributeListItem
|
||||
key={pipe_ (attr, AttributeWithRequirements.A.wikiEntry, Attribute.A.id)}
|
||||
attribute={attr}
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
maxTotalAttributeValues={maxTotalAttributeValues}
|
||||
sum={sum}
|
||||
addPoint={addPoint}
|
||||
removePoint={removePoint}
|
||||
/>
|
||||
)
|
||||
)),
|
||||
maybe<JSX.Element[]> ([]) (toArray)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { fmapF } from "../../../Data/Functor"
|
||||
import { Maybe, or } from "../../../Data/Maybe"
|
||||
import { lte } from "../../../Data/Num"
|
||||
import { Record } from "../../../Data/Record"
|
||||
import { AttributeWithRequirements, AttributeWithRequirementsA_ } from "../../Models/View/AttributeWithRequirements"
|
||||
import { IconButton } from "../Universal/IconButton"
|
||||
import { NumberBox } from "../Universal/NumberBox"
|
||||
import { AttributeBorder } from "./AttributeBorder"
|
||||
|
||||
export interface AttributeListItemProps {
|
||||
attribute: Record<AttributeWithRequirements>
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
maxTotalAttributeValues: Maybe<number>
|
||||
sum: number
|
||||
addPoint (id: string): void
|
||||
removePoint (id: string): void
|
||||
}
|
||||
|
||||
const AWRA = AttributeWithRequirements.A
|
||||
const AWRA_ = AttributeWithRequirementsA_
|
||||
|
||||
export const AttributeListItem: React.FC<AttributeListItemProps> = props => {
|
||||
const {
|
||||
attribute: attr,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
maxTotalAttributeValues,
|
||||
sum,
|
||||
addPoint,
|
||||
removePoint,
|
||||
} = props
|
||||
|
||||
const id = AWRA_.id (attr)
|
||||
const value = AWRA_.value (attr)
|
||||
const mmax = AWRA.max (attr)
|
||||
|
||||
const valueHeader = isInCharacterCreation ? `${value} / ${Maybe.sum (mmax)}` : value
|
||||
|
||||
const handleAdd = React.useCallback (
|
||||
() => addPoint (id),
|
||||
[ addPoint, id ]
|
||||
)
|
||||
|
||||
const handleRemove = React.useCallback (
|
||||
() => removePoint (id),
|
||||
[ removePoint, id ]
|
||||
)
|
||||
|
||||
return (
|
||||
<AttributeBorder
|
||||
className={id}
|
||||
label={AWRA_.short (attr)}
|
||||
value={value}
|
||||
tooltip={
|
||||
<div className="calc-attr-overlay">
|
||||
<h4>
|
||||
<span>{AWRA_.name (attr)}</span>
|
||||
<span>{valueHeader}</span>
|
||||
</h4>
|
||||
</div>
|
||||
}
|
||||
tooltipMargin={11}
|
||||
>
|
||||
{isInCharacterCreation ? <NumberBox max={Maybe.sum (mmax)} /> : null}
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
onClick={handleAdd}
|
||||
disabled={
|
||||
(isInCharacterCreation && sum >= Maybe.sum (maxTotalAttributeValues))
|
||||
|| or (fmapF (mmax) (lte (value)))
|
||||
}
|
||||
/>
|
||||
{isRemovingEnabled
|
||||
? (
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
onClick={handleRemove}
|
||||
disabled={value <= AWRA.min (attr)}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</AttributeBorder>
|
||||
)
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { List } from "../../../Data/List"
|
||||
import { Maybe } from "../../../Data/Maybe"
|
||||
import { Record } from "../../../Data/Record"
|
||||
import { EnergyId } from "../../Constants/Ids"
|
||||
import { HeroModelRecord } from "../../Models/Hero/HeroModel"
|
||||
import { AttributeWithRequirements } from "../../Models/View/AttributeWithRequirements"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { DCPair } from "../../Selectors/derivedCharacteristicsSelectors"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { Page } from "../Universal/Page"
|
||||
import { Scroll } from "../Universal/Scroll"
|
||||
import { AttributeCalc } from "./AttributeCalc"
|
||||
import { AttributeList } from "./AttributeList"
|
||||
import { AttributesAdjustment } from "./AttributesAdjustment"
|
||||
import { AttributesPermanentList } from "./AttributesPermanentList"
|
||||
|
||||
export interface AttributesOwnProps {
|
||||
staticData: StaticDataRecord
|
||||
hero: HeroModelRecord
|
||||
}
|
||||
|
||||
export interface AttributesStateProps {
|
||||
attributes: Maybe<List<Record<AttributeWithRequirements>>>
|
||||
derived: List<DCPair>
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
maxTotalAttributeValues: Maybe<number>
|
||||
sum: number
|
||||
adjustmentValue: Maybe<number>
|
||||
availableAttributeIds: Maybe<List<string>>
|
||||
currentAttributeId: Maybe<string>
|
||||
getEditPermanentEnergy: Maybe<EnergyId>
|
||||
getAddPermanentEnergy: Maybe<EnergyId>
|
||||
}
|
||||
|
||||
export interface AttributesDispatchProps {
|
||||
addPoint (id: string): void
|
||||
removePoint (id: string): void
|
||||
addLifePoint (): void
|
||||
addArcaneEnergyPoint (): void
|
||||
addKarmaPoint (): void
|
||||
removeLifePoint (): void
|
||||
removeArcaneEnergyPoint (): void
|
||||
removeKarmaPoint (): void
|
||||
addLostLPPoint (): void
|
||||
removeLostLPPoint (): void
|
||||
addLostLPPoints (value: number): void
|
||||
addBoughtBackAEPoint (): void
|
||||
removeBoughtBackAEPoint (): void
|
||||
addLostAEPoint (): void
|
||||
removeLostAEPoint (): void
|
||||
addLostAEPoints (value: number): void
|
||||
addBoughtBackKPPoint (): void
|
||||
removeBoughtBackKPPoint (): void
|
||||
addLostKPPoint (): void
|
||||
removeLostKPPoint (): void
|
||||
addLostKPPoints (value: number): void
|
||||
setAdjustmentId (id: Maybe<string>): void
|
||||
openAddPermanentEnergyLoss (energy: EnergyId): void
|
||||
closeAddPermanentEnergyLoss (): void
|
||||
openEditPermanentEnergy (energy: EnergyId): void
|
||||
closeEditPermanentEnergy (): void
|
||||
}
|
||||
|
||||
export type AttributesProps = AttributesStateProps & AttributesDispatchProps & AttributesOwnProps
|
||||
|
||||
export const Attributes: React.FC<AttributesProps> = props => {
|
||||
const {
|
||||
staticData,
|
||||
attributes,
|
||||
derived,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
maxTotalAttributeValues,
|
||||
sum,
|
||||
adjustmentValue,
|
||||
availableAttributeIds,
|
||||
currentAttributeId,
|
||||
getEditPermanentEnergy,
|
||||
getAddPermanentEnergy,
|
||||
addPoint,
|
||||
removePoint,
|
||||
addLifePoint,
|
||||
addArcaneEnergyPoint,
|
||||
addKarmaPoint,
|
||||
removeLifePoint,
|
||||
removeArcaneEnergyPoint,
|
||||
removeKarmaPoint,
|
||||
addLostLPPoint,
|
||||
removeLostLPPoint,
|
||||
addLostLPPoints,
|
||||
addBoughtBackAEPoint,
|
||||
removeBoughtBackAEPoint,
|
||||
addLostAEPoint,
|
||||
removeLostAEPoint,
|
||||
addLostAEPoints,
|
||||
addBoughtBackKPPoint,
|
||||
removeBoughtBackKPPoint,
|
||||
addLostKPPoint,
|
||||
removeLostKPPoint,
|
||||
addLostKPPoints,
|
||||
setAdjustmentId,
|
||||
openAddPermanentEnergyLoss,
|
||||
closeAddPermanentEnergyLoss,
|
||||
openEditPermanentEnergy,
|
||||
closeEditPermanentEnergy,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Page id="attribute">
|
||||
<Scroll>
|
||||
<div className="counter">
|
||||
{translate (staticData) ("attributes.totalpoints")}
|
||||
{": "}
|
||||
{sum}
|
||||
{isInCharacterCreation ? ` / ${Maybe.sum (maxTotalAttributeValues)}` : ""}
|
||||
</div>
|
||||
<AttributeList
|
||||
attributes={attributes}
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
maxTotalAttributeValues={maxTotalAttributeValues}
|
||||
sum={sum}
|
||||
addPoint={addPoint}
|
||||
removePoint={removePoint}
|
||||
/>
|
||||
<div className="secondary">
|
||||
{isInCharacterCreation
|
||||
? (
|
||||
<AttributesAdjustment
|
||||
adjustmentValue={adjustmentValue}
|
||||
attributes={attributes}
|
||||
availableAttributeIds={availableAttributeIds}
|
||||
currentAttributeId={currentAttributeId}
|
||||
staticData={staticData}
|
||||
setAdjustmentId={setAdjustmentId}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
<AttributeCalc
|
||||
derived={derived}
|
||||
staticData={staticData}
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
addLifePoint={addLifePoint}
|
||||
addArcaneEnergyPoint={addArcaneEnergyPoint}
|
||||
addKarmaPoint={addKarmaPoint}
|
||||
removeLifePoint={removeLifePoint}
|
||||
removeArcaneEnergyPoint={removeArcaneEnergyPoint}
|
||||
removeKarmaPoint={removeKarmaPoint}
|
||||
/>
|
||||
<AttributesPermanentList
|
||||
derived={derived}
|
||||
staticData={staticData}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
getEditPermanentEnergy={getEditPermanentEnergy}
|
||||
getAddPermanentEnergy={getAddPermanentEnergy}
|
||||
addLostLPPoint={addLostLPPoint}
|
||||
removeLostLPPoint={removeLostLPPoint}
|
||||
addLostLPPoints={addLostLPPoints}
|
||||
addBoughtBackAEPoint={addBoughtBackAEPoint}
|
||||
removeBoughtBackAEPoint={removeBoughtBackAEPoint}
|
||||
addLostAEPoint={addLostAEPoint}
|
||||
removeLostAEPoint={removeLostAEPoint}
|
||||
addLostAEPoints={addLostAEPoints}
|
||||
addBoughtBackKPPoint={addBoughtBackKPPoint}
|
||||
removeBoughtBackKPPoint={removeBoughtBackKPPoint}
|
||||
addLostKPPoint={addLostKPPoint}
|
||||
removeLostKPPoint={removeLostKPPoint}
|
||||
addLostKPPoints={addLostKPPoints}
|
||||
openAddPermanentEnergyLoss={openAddPermanentEnergyLoss}
|
||||
closeAddPermanentEnergyLoss={closeAddPermanentEnergyLoss}
|
||||
openEditPermanentEnergy={openEditPermanentEnergy}
|
||||
closeEditPermanentEnergy={closeEditPermanentEnergy}
|
||||
/>
|
||||
</div>
|
||||
</Scroll>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { elem, flength, List } from "../../../Data/List"
|
||||
import { fromMaybe, isNothing, joinMaybeList, Just, liftM2, mapMaybe, Maybe, Nothing } from "../../../Data/Maybe"
|
||||
import { Record } from "../../../Data/Record"
|
||||
import { AttributeWithRequirements, AttributeWithRequirementsA_ } from "../../Models/View/AttributeWithRequirements"
|
||||
import { DropdownOption } from "../../Models/View/DropdownOption"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { sign } from "../../Utilities/NumberUtils"
|
||||
import { pipe_ } from "../../Utilities/pipe"
|
||||
import { Dropdown } from "../Universal/Dropdown"
|
||||
|
||||
export interface AttributesAdjustmentProps {
|
||||
adjustmentValue: Maybe<number>
|
||||
attributes: Maybe<List<Record<AttributeWithRequirements>>>
|
||||
availableAttributeIds: Maybe<List<string>>
|
||||
currentAttributeId: Maybe<string>
|
||||
staticData: StaticDataRecord
|
||||
setAdjustmentId (id: Maybe<string>): void
|
||||
}
|
||||
|
||||
const AWRA_ = AttributeWithRequirementsA_
|
||||
|
||||
export const AttributesAdjustment: React.FC<AttributesAdjustmentProps> = props => {
|
||||
const {
|
||||
attributes: mattributes,
|
||||
staticData,
|
||||
currentAttributeId,
|
||||
adjustmentValue: madjustment,
|
||||
availableAttributeIds: mavailable_attr_ids,
|
||||
setAdjustmentId,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="attribute-adjustment">
|
||||
<span className="label">
|
||||
{translate (staticData) ("attributes.attributeadjustmentselection")}
|
||||
</span>
|
||||
{fromMaybe
|
||||
(<></>)
|
||||
(liftM2 ((available_attr_ids: List<string>) => (adjustment: number) => (
|
||||
<Dropdown
|
||||
options={
|
||||
pipe_ (
|
||||
mattributes,
|
||||
joinMaybeList,
|
||||
mapMaybe (x => elem (AWRA_.id (x)) (available_attr_ids)
|
||||
? Just (DropdownOption ({
|
||||
id: Just (AWRA_.id (x)),
|
||||
name: `${AWRA_.name (x)} ${sign (adjustment)}`,
|
||||
}))
|
||||
: Nothing)
|
||||
)
|
||||
}
|
||||
value={currentAttributeId}
|
||||
onChange={setAdjustmentId}
|
||||
disabled={isNothing (currentAttributeId) || flength (available_attr_ids) === 1}
|
||||
/>
|
||||
))
|
||||
(mavailable_attr_ids)
|
||||
(madjustment))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { DerivedCharacteristicId } from "../../../../app/Database/Schema/DerivedCharacteristics/DerivedCharacteristics.l10n"
|
||||
import { equals } from "../../../Data/Eq"
|
||||
import { find, List } from "../../../Data/List"
|
||||
import { bindF, ensure, isJust, Maybe, maybe } from "../../../Data/Maybe"
|
||||
import { Record } from "../../../Data/Record"
|
||||
import { fst, Pair, snd } from "../../../Data/Tuple"
|
||||
import { DCId, EnergyId } from "../../Constants/Ids"
|
||||
import { DerivedCharacteristicValues, EnergyWithLoss } from "../../Models/View/DerivedCharacteristicCombined"
|
||||
import { DerivedCharacteristic } from "../../Models/Wiki/DerivedCharacteristic"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { DCPair } from "../../Selectors/derivedCharacteristicsSelectors"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { pipe, pipe_ } from "../../Utilities/pipe"
|
||||
import { AttributesPermanentListItem } from "./AttributesPermanentListItem"
|
||||
|
||||
type EWLPair = Pair<Record<DerivedCharacteristic>, Record<EnergyWithLoss>>
|
||||
|
||||
export interface AttributesPermanentListProps {
|
||||
derived: List<DCPair>
|
||||
staticData: StaticDataRecord
|
||||
isRemovingEnabled: boolean
|
||||
getEditPermanentEnergy: Maybe<EnergyId>
|
||||
getAddPermanentEnergy: Maybe<EnergyId>
|
||||
addLostLPPoint (): void
|
||||
removeLostLPPoint (): void
|
||||
addLostLPPoints (value: number): void
|
||||
addBoughtBackAEPoint (): void
|
||||
removeBoughtBackAEPoint (): void
|
||||
addLostAEPoint (): void
|
||||
removeLostAEPoint (): void
|
||||
addLostAEPoints (value: number): void
|
||||
addBoughtBackKPPoint (): void
|
||||
removeBoughtBackKPPoint (): void
|
||||
addLostKPPoint (): void
|
||||
removeLostKPPoint (): void
|
||||
addLostKPPoints (value: number): void
|
||||
openAddPermanentEnergyLoss (energy: EnergyId): void
|
||||
closeAddPermanentEnergyLoss (): void
|
||||
openEditPermanentEnergy (energy: EnergyId): void
|
||||
closeEditPermanentEnergy (): void
|
||||
}
|
||||
|
||||
const DCA = DerivedCharacteristic.A
|
||||
const DCVA = DerivedCharacteristicValues.A
|
||||
|
||||
export const AttributesPermanentList: React.FC<AttributesPermanentListProps> = props => {
|
||||
const {
|
||||
derived,
|
||||
staticData,
|
||||
isRemovingEnabled,
|
||||
getEditPermanentEnergy,
|
||||
getAddPermanentEnergy,
|
||||
addLostLPPoint,
|
||||
removeLostLPPoint,
|
||||
addLostLPPoints,
|
||||
addBoughtBackAEPoint,
|
||||
removeBoughtBackAEPoint,
|
||||
addLostAEPoint,
|
||||
removeLostAEPoint,
|
||||
addLostAEPoints,
|
||||
addBoughtBackKPPoint,
|
||||
removeBoughtBackKPPoint,
|
||||
addLostKPPoint,
|
||||
removeLostKPPoint,
|
||||
addLostKPPoints,
|
||||
openAddPermanentEnergyLoss,
|
||||
closeAddPermanentEnergyLoss,
|
||||
openEditPermanentEnergy,
|
||||
closeEditPermanentEnergy,
|
||||
} = props
|
||||
|
||||
const mlp = find<DCPair> (pipe (fst, DCA.id, equals<DerivedCharacteristicId> (DCId.LP)))
|
||||
(derived) as Maybe<EWLPair>
|
||||
|
||||
const mae = find<DCPair> (pipe (fst, DCA.id, equals<DerivedCharacteristicId> (DCId.AE)))
|
||||
(derived) as Maybe<EWLPair>
|
||||
|
||||
const mkp = find<DCPair> (pipe (fst, DCA.id, equals<DerivedCharacteristicId> (DCId.KP)))
|
||||
(derived) as Maybe<EWLPair>
|
||||
|
||||
return (
|
||||
<div className="permanent">
|
||||
{
|
||||
maybe (<></>)
|
||||
((lp: Pair<Record<DerivedCharacteristic>, Record<EnergyWithLoss>>) => (
|
||||
<AttributesPermanentListItem
|
||||
staticData={staticData}
|
||||
id={EnergyId.LP}
|
||||
label={translate (staticData) ("attributes.lostpermanently.lifepoints.short")}
|
||||
name={translate (staticData) ("attributes.lostpermanently.lifepoints")}
|
||||
lost={Maybe.sum (DCVA.permanentLost (snd (lp)))}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
getEditPermanentEnergy={getEditPermanentEnergy}
|
||||
getAddPermanentEnergy={getAddPermanentEnergy}
|
||||
addLostPoint={addLostLPPoint}
|
||||
addLostPoints={addLostLPPoints}
|
||||
removeLostPoint={removeLostLPPoint}
|
||||
openAddPermanentEnergyLoss={openAddPermanentEnergyLoss}
|
||||
closeAddPermanentEnergyLoss={closeAddPermanentEnergyLoss}
|
||||
openEditPermanentEnergy={openEditPermanentEnergy}
|
||||
closeEditPermanentEnergy={closeEditPermanentEnergy}
|
||||
/>
|
||||
))
|
||||
(mlp)
|
||||
}
|
||||
{pipe_ (
|
||||
mae,
|
||||
bindF (ensure (ae => isJust (DCVA.value (snd (ae))))),
|
||||
maybe (<></>)
|
||||
((ae: Pair<Record<DerivedCharacteristic>, Record<EnergyWithLoss>>) => (
|
||||
<AttributesPermanentListItem
|
||||
staticData={staticData}
|
||||
id={EnergyId.AE}
|
||||
label={translate (staticData) ("attributes.lostpermanently.arcaneenergy.short")}
|
||||
name={translate (staticData) ("attributes.lostpermanently.arcaneenergy")}
|
||||
boughtBack={Maybe.sum (DCVA.permanentRedeemed (snd (ae)))}
|
||||
lost={Maybe.sum (DCVA.permanentLost (snd (ae)))}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
getEditPermanentEnergy={getEditPermanentEnergy}
|
||||
getAddPermanentEnergy={getAddPermanentEnergy}
|
||||
addBoughtBackPoint={addBoughtBackAEPoint}
|
||||
addLostPoint={addLostAEPoint}
|
||||
addLostPoints={addLostAEPoints}
|
||||
removeBoughtBackPoint={removeBoughtBackAEPoint}
|
||||
removeLostPoint={removeLostAEPoint}
|
||||
openAddPermanentEnergyLoss={openAddPermanentEnergyLoss}
|
||||
closeAddPermanentEnergyLoss={closeAddPermanentEnergyLoss}
|
||||
openEditPermanentEnergy={openEditPermanentEnergy}
|
||||
closeEditPermanentEnergy={closeEditPermanentEnergy}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{pipe_ (
|
||||
mkp,
|
||||
bindF (ensure (kp => isJust (DCVA.value (snd (kp))))),
|
||||
maybe (<></>)
|
||||
((kp: Pair<Record<DerivedCharacteristic>, Record<EnergyWithLoss>>) => (
|
||||
<AttributesPermanentListItem
|
||||
staticData={staticData}
|
||||
id={EnergyId.KP}
|
||||
label={translate (staticData) ("attributes.lostpermanently.karmapoints.short")}
|
||||
name={translate (staticData) ("attributes.lostpermanently.karmapoints")}
|
||||
boughtBack={Maybe.sum (DCVA.permanentRedeemed (snd (kp)))}
|
||||
lost={Maybe.sum (DCVA.permanentLost (snd (kp)))}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
getEditPermanentEnergy={getEditPermanentEnergy}
|
||||
getAddPermanentEnergy={getAddPermanentEnergy}
|
||||
addBoughtBackPoint={addBoughtBackKPPoint}
|
||||
addLostPoint={addLostKPPoint}
|
||||
addLostPoints={addLostKPPoints}
|
||||
removeBoughtBackPoint={removeBoughtBackKPPoint}
|
||||
removeLostPoint={removeLostKPPoint}
|
||||
openAddPermanentEnergyLoss={openAddPermanentEnergyLoss}
|
||||
closeAddPermanentEnergyLoss={closeAddPermanentEnergyLoss}
|
||||
openEditPermanentEnergy={openEditPermanentEnergy}
|
||||
closeEditPermanentEnergy={closeEditPermanentEnergy}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Maybe } from "../../../Data/Maybe"
|
||||
import { EnergyId } from "../../Constants/Ids"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { isFunction } from "../../Utilities/typeCheckUtils"
|
||||
import { IconButton } from "../Universal/IconButton"
|
||||
import { AttributeBorder } from "./AttributeBorder"
|
||||
import { AttributesRemovePermanent } from "./AttributesRemovePermanent"
|
||||
import { PermanentPoints } from "./PermanentPoints"
|
||||
|
||||
export interface AttributesPermanentListItemProps {
|
||||
staticData: StaticDataRecord
|
||||
id: EnergyId
|
||||
label: string
|
||||
name: string
|
||||
boughtBack?: number
|
||||
lost: number
|
||||
isRemovingEnabled: boolean
|
||||
getEditPermanentEnergy: Maybe<EnergyId>
|
||||
getAddPermanentEnergy: Maybe<EnergyId>
|
||||
addBoughtBackPoint? (): void
|
||||
addLostPoint (): void
|
||||
addLostPoints (value: number): void
|
||||
removeBoughtBackPoint? (): void
|
||||
removeLostPoint (): void
|
||||
openAddPermanentEnergyLoss (energy: EnergyId): void
|
||||
closeAddPermanentEnergyLoss (): void
|
||||
openEditPermanentEnergy (energy: EnergyId): void
|
||||
closeEditPermanentEnergy (): void
|
||||
}
|
||||
|
||||
export const AttributesPermanentListItem: React.FC<AttributesPermanentListItemProps> = props => {
|
||||
const {
|
||||
staticData,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
boughtBack,
|
||||
lost,
|
||||
isRemovingEnabled,
|
||||
getEditPermanentEnergy,
|
||||
getAddPermanentEnergy,
|
||||
addBoughtBackPoint,
|
||||
addLostPoint,
|
||||
addLostPoints,
|
||||
removeBoughtBackPoint,
|
||||
removeLostPoint,
|
||||
openAddPermanentEnergyLoss,
|
||||
closeAddPermanentEnergyLoss,
|
||||
openEditPermanentEnergy,
|
||||
closeEditPermanentEnergy,
|
||||
} = props
|
||||
|
||||
const available = typeof boughtBack === "number" ? lost - boughtBack : lost
|
||||
|
||||
const handleOpenEditPermanentEnergy = React.useCallback (
|
||||
() => openEditPermanentEnergy (id),
|
||||
[ openEditPermanentEnergy, id ]
|
||||
)
|
||||
|
||||
const handleOpenAddPermanentEnergyLoss = React.useCallback (
|
||||
() => openAddPermanentEnergyLoss (id),
|
||||
[ openAddPermanentEnergyLoss, id ]
|
||||
)
|
||||
|
||||
return (
|
||||
<AttributeBorder
|
||||
label={label}
|
||||
value={available}
|
||||
tooltip={
|
||||
<div className="calc-attr-overlay">
|
||||
<h4>
|
||||
<span>{name}</span>
|
||||
<span>{available}</span>
|
||||
</h4>
|
||||
{
|
||||
typeof boughtBack === "number"
|
||||
? (
|
||||
<p>
|
||||
{translate (staticData) ("attributes.derivedcharacteristics.tooltips.losttotal")}
|
||||
{": "}
|
||||
{lost}
|
||||
<br />
|
||||
{translate (staticData) ("attributes.derivedcharacteristics.tooltips.boughtback")}
|
||||
{": "}
|
||||
{boughtBack}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p>
|
||||
{translate (staticData) ("attributes.derivedcharacteristics.tooltips.losttotal")}
|
||||
{": "}
|
||||
{lost}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
tooltipMargin={7}
|
||||
>
|
||||
{isRemovingEnabled
|
||||
? (
|
||||
<IconButton
|
||||
className="edit"
|
||||
icon=""
|
||||
onClick={handleOpenEditPermanentEnergy}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
<PermanentPoints
|
||||
id={String (id)}
|
||||
eid={id}
|
||||
staticData={staticData}
|
||||
permanentBoughtBack={Maybe (boughtBack)}
|
||||
permanentSpent={lost}
|
||||
isOpen={Maybe.elem (id) (getEditPermanentEnergy)}
|
||||
addBoughtBackPoint={addBoughtBackPoint}
|
||||
addLostPoint={addLostPoint}
|
||||
removeBoughtBackPoint={removeBoughtBackPoint}
|
||||
removeLostPoint={removeLostPoint}
|
||||
close={closeEditPermanentEnergy}
|
||||
/>
|
||||
{isRemovingEnabled
|
||||
? null
|
||||
: (
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
onClick={handleOpenAddPermanentEnergyLoss}
|
||||
/>
|
||||
)}
|
||||
<AttributesRemovePermanent
|
||||
remove={addLostPoints}
|
||||
staticData={staticData}
|
||||
isOpen={Maybe.elem (id) (getAddPermanentEnergy)}
|
||||
close={closeAddPermanentEnergyLoss}
|
||||
/>
|
||||
{!isRemovingEnabled && isFunction (addBoughtBackPoint)
|
||||
? (
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
onClick={addBoughtBackPoint}
|
||||
disabled={available <= 0}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</AttributeBorder>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { fromJust, isJust, Just, Nothing } from "../../../Data/Maybe"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { toInt } from "../../Utilities/NumberUtils"
|
||||
import { isNaturalNumber } from "../../Utilities/RegexUtils"
|
||||
import { BasicInputDialog } from "../Universal/BasicInputDialog"
|
||||
|
||||
export interface AttributesRemovePermanentProps {
|
||||
isOpen: boolean
|
||||
staticData: StaticDataRecord
|
||||
close (): void
|
||||
remove (value: number): void
|
||||
}
|
||||
|
||||
export interface AttributesRemovePermanentState {
|
||||
value: string
|
||||
}
|
||||
|
||||
export const AttributesRemovePermanent: React.FC<AttributesRemovePermanentProps> = props => {
|
||||
const { staticData, remove, isOpen, close } = props
|
||||
|
||||
const [ value, setValue ] = React.useState ("")
|
||||
|
||||
const handleRemove = React.useCallback (
|
||||
() => {
|
||||
const mvalue = toInt (value)
|
||||
|
||||
if (isJust (mvalue)) {
|
||||
remove (fromJust (mvalue))
|
||||
}
|
||||
},
|
||||
[ remove, value ]
|
||||
)
|
||||
|
||||
return (
|
||||
<BasicInputDialog
|
||||
id="overview-add-ap"
|
||||
isOpen={isOpen}
|
||||
title={translate (staticData) ("attributes.removeenergypointslostpermanently.message")}
|
||||
description=""
|
||||
value={value}
|
||||
invalid={isNaturalNumber (value) ? Nothing : Just ("")}
|
||||
acceptLabel={translate (staticData)
|
||||
("attributes.removeenergypointslostpermanently.removebtn")}
|
||||
rejectLabel={translate (staticData) ("general.dialogs.cancelbtn")}
|
||||
onClose={close}
|
||||
onAccept={handleRemove}
|
||||
onChange={setValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { fromJust, isJust, Maybe } from "../../../Data/Maybe"
|
||||
import { EnergyId } from "../../Constants/Ids"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { translate } from "../../Utilities/I18n"
|
||||
import { isFunction } from "../../Utilities/typeCheckUtils"
|
||||
import { Dialog } from "../Universal/Dialog"
|
||||
import { IconButton } from "../Universal/IconButton"
|
||||
|
||||
export interface PermanentPointsProps {
|
||||
id: string
|
||||
eid: EnergyId
|
||||
staticData: StaticDataRecord
|
||||
permanentBoughtBack: Maybe<number>
|
||||
permanentSpent: number
|
||||
isOpen: boolean
|
||||
addBoughtBackPoint? (): void
|
||||
addLostPoint (): void
|
||||
removeBoughtBackPoint? (): void
|
||||
removeLostPoint (): void
|
||||
close (): void
|
||||
}
|
||||
|
||||
export const PermanentPoints: React.FC<PermanentPointsProps> = props => {
|
||||
const {
|
||||
id,
|
||||
eid,
|
||||
staticData,
|
||||
addBoughtBackPoint,
|
||||
addLostPoint,
|
||||
permanentBoughtBack,
|
||||
permanentSpent,
|
||||
removeBoughtBackPoint,
|
||||
removeLostPoint,
|
||||
close,
|
||||
isOpen,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id={id}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
className="permanent-points-editor"
|
||||
title={
|
||||
eid === EnergyId.AE
|
||||
? translate (staticData) ("attributes.lostpermanently.arcaneenergy")
|
||||
: eid === EnergyId.KP
|
||||
? translate (staticData) ("attributes.lostpermanently.karmapoints")
|
||||
: translate (staticData) ("attributes.lostpermanently.lifepoints")
|
||||
}
|
||||
buttons={[
|
||||
{
|
||||
autoWidth: true,
|
||||
label: translate (staticData) ("general.dialogs.donebtn"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="main">
|
||||
{
|
||||
isFunction (addBoughtBackPoint)
|
||||
&& isFunction (removeBoughtBackPoint)
|
||||
&& isJust (permanentBoughtBack)
|
||||
? (
|
||||
<div className="column boughtback">
|
||||
<div className="value">{fromJust (permanentBoughtBack)}</div>
|
||||
<div className="description smallcaps">
|
||||
{translate (staticData) ("attributes.pointslostpermanentlyeditor.boughtback")}
|
||||
</div>
|
||||
<div className="buttons">
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
onClick={addBoughtBackPoint}
|
||||
disabled={fromJust (permanentBoughtBack) >= permanentSpent}
|
||||
/>
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
onClick={removeBoughtBackPoint}
|
||||
disabled={fromJust (permanentBoughtBack) <= 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<div className="column lost">
|
||||
<div className="value">{permanentSpent}</div>
|
||||
<div className="description smallcaps">
|
||||
{translate (staticData) ("attributes.pointslostpermanentlyeditor.spent")}
|
||||
</div>
|
||||
<div className="buttons">
|
||||
<IconButton className="add" icon="" onClick={addLostPoint} />
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
onClick={removeLostPoint}
|
||||
disabled={permanentSpent <= 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { List } from "../../../Data/List"
|
||||
import { Maybe, maybeRNull } from "../../../Data/Maybe"
|
||||
import { Record } from "../../../Data/Record"
|
||||
import { AdventurePointsCategories } from "../../Models/View/AdventurePointsCategories"
|
||||
import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
import { translate, translateP } from "../../Utilities/I18n"
|
||||
|
||||
interface Props {
|
||||
staticData: StaticDataRecord
|
||||
adventurePoints: Record<AdventurePointsCategories>
|
||||
maximumForMagicalAdvantagesDisadvantages: Maybe<number>
|
||||
isSpellcaster: boolean
|
||||
isBlessedOne: boolean
|
||||
}
|
||||
|
||||
const APCA = AdventurePointsCategories.A
|
||||
|
||||
export const ApTooltip: React.FC<Props> = props => {
|
||||
const {
|
||||
staticData,
|
||||
adventurePoints: ap,
|
||||
maximumForMagicalAdvantagesDisadvantages,
|
||||
isSpellcaster,
|
||||
isBlessedOne,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="ap-details">
|
||||
<h4>{translate (staticData) ("header.aptooltip.title")}</h4>
|
||||
<p className="general">
|
||||
<span>{translateP (staticData) ("header.aptooltip.total") (List (APCA.total (ap)))}</span>
|
||||
<span>{translateP (staticData) ("header.aptooltip.spent") (List (APCA.spent (ap)))}</span>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonadvantages")
|
||||
(List (APCA.spentOnAdvantages (ap), 80))}
|
||||
</span>
|
||||
<span>
|
||||
{APCA.spentOnMagicalAdvantages (ap) > 0
|
||||
? translateP (staticData)
|
||||
("header.aptooltip.spentonmagicadvantages")
|
||||
(List (
|
||||
APCA.spentOnMagicalAdvantages (ap),
|
||||
Maybe.sum (maximumForMagicalAdvantagesDisadvantages)
|
||||
))
|
||||
: null}
|
||||
</span>
|
||||
<span>
|
||||
{APCA.spentOnBlessedAdvantages (ap) > 0
|
||||
? translateP (staticData)
|
||||
("header.aptooltip.spentonblessedadvantages")
|
||||
(List (APCA.spentOnBlessedAdvantages (ap), 50))
|
||||
: null}
|
||||
</span>
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentondisadvantages")
|
||||
(List (APCA.spentOnDisadvantages (ap), 80))}
|
||||
</span>
|
||||
<span>
|
||||
{APCA.spentOnMagicalDisadvantages (ap) > 0
|
||||
? translateP (staticData)
|
||||
("header.aptooltip.spentonmagicdisadvantages")
|
||||
(List (
|
||||
APCA.spentOnMagicalDisadvantages (ap),
|
||||
Maybe.sum (maximumForMagicalAdvantagesDisadvantages)
|
||||
))
|
||||
: null}
|
||||
</span>
|
||||
<span>
|
||||
{APCA.spentOnBlessedDisadvantages (ap) > 0
|
||||
? translateP (staticData)
|
||||
("header.aptooltip.spentonblesseddisadvantages")
|
||||
(List (APCA.spentOnBlessedDisadvantages (ap), 50))
|
||||
: null}
|
||||
</span>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonrace")
|
||||
(List (APCA.spentOnRace (ap), 80))}
|
||||
</span>
|
||||
{maybeRNull ((spentOnProfession: number) => (
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonprofession")
|
||||
(List (spentOnProfession, 80))}
|
||||
</span>
|
||||
))
|
||||
(APCA.spentOnProfession (ap))}
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonattributes")
|
||||
(List (APCA.spentOnAttributes (ap)))}
|
||||
</span>
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonskills")
|
||||
(List (APCA.spentOnSkills (ap)))}
|
||||
</span>
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentoncombattechniques")
|
||||
(List (APCA.spentOnCombatTechniques (ap)))}
|
||||
</span>
|
||||
{isSpellcaster
|
||||
? (
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonspells")
|
||||
(List (APCA.spentOnSpells (ap)))}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isSpellcaster
|
||||
? (
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentoncantrips")
|
||||
(List (APCA.spentOnCantrips (ap)))}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isBlessedOne
|
||||
? (
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonliturgicalchants")
|
||||
(List (APCA.spentOnLiturgicalChants (ap)))}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isBlessedOne
|
||||
? (
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonblessings")
|
||||
(List (APCA.spentOnBlessings (ap)))}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonspecialabilities")
|
||||
(List (APCA.spentOnSpecialAbilities (ap)))}
|
||||
</span>
|
||||
<span>
|
||||
{translateP (staticData)
|
||||
("header.aptooltip.spentonenergies")
|
||||
(List (APCA.spentOnEnergies (ap)))}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useState } from "react"
|
||||
|
||||
export const useModalState = () => {
|
||||
const [ isOpen, setIsOpen ] = useState(false)
|
||||
|
||||
const open = () => setIsOpen(true)
|
||||
const close = () => setIsOpen(false)
|
||||
|
||||
return { isOpen, open, close }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LocaleMap } from "optolith-database-schema/types/_LocaleMap"
|
||||
import { selectLocale } from "../slices/settingsSlice.ts"
|
||||
import { useAppSelector } from "./redux.ts"
|
||||
|
||||
export const useTranslateMap = () => {
|
||||
const locale = useAppSelector(selectLocale)
|
||||
|
||||
const translateMap = <T>(map: LocaleMap<T>): T | undefined => map[locale]
|
||||
|
||||
return translateMap
|
||||
}
|
||||
@@ -60,6 +60,8 @@ const mainHierarchy: DisplayRoute[] = [
|
||||
},
|
||||
]
|
||||
|
||||
type Section = "main" | "character"
|
||||
|
||||
export const useVisibleTabs = () => {
|
||||
const currentRoute = useSelector(selectRoute)
|
||||
|
||||
@@ -150,7 +152,10 @@ export const useVisibleTabs = () => {
|
||||
]
|
||||
)
|
||||
|
||||
const hierarchies = [ mainHierarchy, characterHierarchy ]
|
||||
const hierarchies: [Section, DisplayRoute[]][] = [
|
||||
[ "main", mainHierarchy ],
|
||||
[ "character", characterHierarchy ],
|
||||
]
|
||||
|
||||
const isInDisplayRoute = (route: Route, tab: DisplayRoute) =>
|
||||
tab.type === "single" ? tab.route === route : tab.routes.includes(route)
|
||||
@@ -159,14 +164,15 @@ export const useVisibleTabs = () => {
|
||||
hierarchy.some(tab => isInDisplayRoute(route, tab))
|
||||
|
||||
const getHierarchyByRoute = (route: Route) =>
|
||||
hierarchies.find(hierarchy => isInHierarchy(route, hierarchy))
|
||||
hierarchies.find(hierarchy => isInHierarchy(route, hierarchy[1]))
|
||||
|
||||
const currentHierarchy = getHierarchyByRoute(currentRoute) ?? mainHierarchy
|
||||
const currentHierarchy = getHierarchyByRoute(currentRoute) ?? [ "main", mainHierarchy ]
|
||||
|
||||
const currentDisplayRoute = currentHierarchy?.find(tab => isInDisplayRoute(currentRoute, tab))
|
||||
const currentDisplayRoute = currentHierarchy[1]?.find(tab => isInDisplayRoute(currentRoute, tab))
|
||||
|
||||
return {
|
||||
mainTabs: currentHierarchy,
|
||||
section: currentHierarchy[0],
|
||||
mainTabs: currentHierarchy[1],
|
||||
subTabs: currentDisplayRoute?.type === "group" ? currentDisplayRoute.routes : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,8 @@ import { store } from "./store.ts"
|
||||
const domNode = document.getElementById("root")!
|
||||
const root = createRoot(domNode)
|
||||
|
||||
document.body.classList.add(`platform--${ExternalAPI.platform}`)
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<Root />
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { FC } from "react"
|
||||
import { useAppSelector } from "../hooks/redux.ts"
|
||||
import { useTranslate } from "../hooks/translate.ts"
|
||||
import { selectAdventurePointsSpent, selectAdventurePointsSpentOnAdvantages, selectAdventurePointsSpentOnAttributes, selectAdventurePointsSpentOnBlessedAdvantages, selectAdventurePointsSpentOnBlessedDisadvantages, selectAdventurePointsSpentOnBlessings, selectAdventurePointsSpentOnCantrips, selectAdventurePointsSpentOnCombatTechniques, selectAdventurePointsSpentOnDisadvantages, selectAdventurePointsSpentOnEnergies, selectAdventurePointsSpentOnLiturgicalChants, selectAdventurePointsSpentOnMagicalAdvantages, selectAdventurePointsSpentOnMagicalDisadvantages, selectAdventurePointsSpentOnProfession, selectAdventurePointsSpentOnRace, selectAdventurePointsSpentOnSkills, selectAdventurePointsSpentOnSpecialAbilities, selectAdventurePointsSpentOnSpells } from "../selectors/adventurePointSelectors.ts"
|
||||
import { selectTotalAdventurePoints } from "../slices/characterSlice.ts"
|
||||
|
||||
export const AdventurePointsTooltip: FC = () => {
|
||||
const translate = useTranslate()
|
||||
|
||||
// TODO: Replace with real selectors
|
||||
const isSpellcaster = true
|
||||
const isBlessedOne = true
|
||||
const maximumForMagicalAdvantagesDisadvantages = 50
|
||||
|
||||
const total = useAppSelector(selectTotalAdventurePoints)
|
||||
const spent = useAppSelector(selectAdventurePointsSpent)
|
||||
const spentOnAttributes = useAppSelector(selectAdventurePointsSpentOnAttributes)
|
||||
const spentOnSkills = useAppSelector(selectAdventurePointsSpentOnSkills)
|
||||
const spentOnCombatTechniques = useAppSelector(selectAdventurePointsSpentOnCombatTechniques)
|
||||
const spentOnSpells = useAppSelector(selectAdventurePointsSpentOnSpells)
|
||||
const spentOnLiturgicalChants = useAppSelector(selectAdventurePointsSpentOnLiturgicalChants)
|
||||
const spentOnCantrips = useAppSelector(selectAdventurePointsSpentOnCantrips)
|
||||
const spentOnBlessings = useAppSelector(selectAdventurePointsSpentOnBlessings)
|
||||
const spentOnAdvantages = useAppSelector(selectAdventurePointsSpentOnAdvantages)
|
||||
const spentOnMagicalAdvantages = useAppSelector(selectAdventurePointsSpentOnMagicalAdvantages)
|
||||
const spentOnBlessedAdvantages = useAppSelector(selectAdventurePointsSpentOnBlessedAdvantages)
|
||||
const spentOnDisadvantages = useAppSelector(selectAdventurePointsSpentOnDisadvantages)
|
||||
const spentOnMagicalDisadvantages =
|
||||
useAppSelector(selectAdventurePointsSpentOnMagicalDisadvantages)
|
||||
const spentOnBlessedDisadvantages =
|
||||
useAppSelector(selectAdventurePointsSpentOnBlessedDisadvantages)
|
||||
const spentOnSpecialAbilities = useAppSelector(selectAdventurePointsSpentOnSpecialAbilities)
|
||||
const spentOnEnergies = useAppSelector(selectAdventurePointsSpentOnEnergies)
|
||||
const spentOnRace = useAppSelector(selectAdventurePointsSpentOnRace)
|
||||
const spentOnProfession = useAppSelector(selectAdventurePointsSpentOnProfession)
|
||||
|
||||
return (
|
||||
<div className="ap-details">
|
||||
<h4>{translate("header.aptooltip.title")}</h4>
|
||||
<p className="general">
|
||||
<span>{translate("header.aptooltip.total", total ?? 0)}</span>
|
||||
<span>{translate("header.aptooltip.spent", spent.general)}</span>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonadvantages",
|
||||
spentOnAdvantages.general + spentOnAdvantages.bound,
|
||||
80
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{spentOnMagicalAdvantages.general + spentOnMagicalAdvantages.bound > 0
|
||||
? translate(
|
||||
"header.aptooltip.spentonmagicadvantages",
|
||||
spentOnMagicalAdvantages.general + spentOnMagicalAdvantages.bound,
|
||||
maximumForMagicalAdvantagesDisadvantages,
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
<span>
|
||||
{spentOnBlessedAdvantages.general + spentOnBlessedAdvantages.bound > 0
|
||||
? translate(
|
||||
"header.aptooltip.spentonblessedadvantages",
|
||||
spentOnBlessedAdvantages.general + spentOnBlessedAdvantages.bound,
|
||||
50,
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentondisadvantages",
|
||||
spentOnDisadvantages.general + spentOnDisadvantages.bound,
|
||||
80,
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound > 0
|
||||
? translate(
|
||||
"header.aptooltip.spentonmagicdisadvantages",
|
||||
spentOnMagicalDisadvantages.general + spentOnMagicalDisadvantages.bound,
|
||||
maximumForMagicalAdvantagesDisadvantages,
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
<span>
|
||||
{spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound > 0
|
||||
? translate(
|
||||
"header.aptooltip.spentonblesseddisadvantages",
|
||||
spentOnBlessedDisadvantages.general + spentOnBlessedDisadvantages.bound,
|
||||
50,
|
||||
)
|
||||
: null}
|
||||
</span>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<span>
|
||||
{translate("header.aptooltip.spentonrace", spentOnRace)}
|
||||
</span>
|
||||
{spentOnProfession === undefined
|
||||
? null
|
||||
: (
|
||||
<span>
|
||||
{translate("header.aptooltip.spentonprofession", spentOnProfession)}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonattributes",
|
||||
spentOnAttributes.general + spentOnAttributes.bound,
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonskills",
|
||||
spentOnSkills.general + spentOnSkills.bound,
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentoncombattechniques",
|
||||
spentOnCombatTechniques.general + spentOnCombatTechniques.bound,
|
||||
)}
|
||||
</span>
|
||||
{isSpellcaster
|
||||
? (
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonspells",
|
||||
spentOnSpells.general + spentOnSpells.bound,
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isSpellcaster
|
||||
? (
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentoncantrips",
|
||||
spentOnCantrips.general + spentOnCantrips.bound,
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isBlessedOne
|
||||
? (
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonliturgicalchants",
|
||||
spentOnLiturgicalChants.general + spentOnLiturgicalChants.bound,
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{isBlessedOne
|
||||
? (
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonblessings",
|
||||
spentOnBlessings.general + spentOnBlessings.bound,
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonspecialabilities",
|
||||
spentOnSpecialAbilities.general + spentOnSpecialAbilities.bound,
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{translate(
|
||||
"header.aptooltip.spentonenergies",
|
||||
spentOnEnergies,
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { FC } from "react"
|
||||
import { Button } from "../../shared/components/button/Button.tsx"
|
||||
import { IconButton } from "../../shared/components/iconButton/IconButton.tsx"
|
||||
import { TooltipToggle } from "../../shared/components/tooltipToggle/TooltipToggle.tsx"
|
||||
import { ExternalAPI } from "../external.ts"
|
||||
import { useAppSelector } from "../hooks/redux.ts"
|
||||
import { useTranslate } from "../hooks/translate.ts"
|
||||
import { useVisibleTabs } from "../hooks/visibleTabs.ts"
|
||||
import { selectAdventurePointsAvailable } from "../selectors/adventurePointSelectors.ts"
|
||||
import { AdventurePointsTooltip } from "./AdventurePointsTooltip.tsx"
|
||||
import { NavigationBarLeft } from "./NavigationBarLeft.tsx"
|
||||
import { NavigationBarRight } from "./NavigationBarRight.tsx"
|
||||
import { NavigationBarSubTabs } from "./NavigationBarSubTabs.tsx"
|
||||
@@ -15,8 +20,8 @@ export const NavigationBar: FC = () => {
|
||||
const translate = useTranslate()
|
||||
// const dispatch = useAppDispatch()
|
||||
// const handleHerolistTab = useCallback(() => dispatch(goToTab("characters")), [ dispatch ])
|
||||
|
||||
const { mainTabs, subTabs } = useVisibleTabs()
|
||||
const { section, mainTabs, subTabs } = useVisibleTabs()
|
||||
const available = useAppSelector(selectAdventurePointsAvailable)
|
||||
|
||||
return (
|
||||
<nav>
|
||||
@@ -33,68 +38,46 @@ export const NavigationBar: FC = () => {
|
||||
<NavigationBarTabs tabs={mainTabs} />
|
||||
</NavigationBarLeft>
|
||||
<NavigationBarRight>
|
||||
{/* {isHeroSection
|
||||
{section === "character"
|
||||
? (
|
||||
<>
|
||||
{maybe(<Text className="collected-ap">
|
||||
{translateP(staticData)("header.apleft")(List("X"))}
|
||||
</Text>)
|
||||
((ap: Record<AdventurePointsCategories>) => (
|
||||
<TooltipToggle
|
||||
position="bottom"
|
||||
margin={12}
|
||||
content={
|
||||
<ApTooltip
|
||||
adventurePoints={ap}
|
||||
staticData={staticData}
|
||||
maximumForMagicalAdvantagesDisadvantages={
|
||||
maximumForMagicalAdvantagesDisadvantages
|
||||
}
|
||||
isSpellcaster={isSpellcaster}
|
||||
isBlessedOne={isBlessedOne}
|
||||
/>
|
||||
}
|
||||
target={
|
||||
<Text className="collected-ap">
|
||||
{translateP(staticData)
|
||||
("header.apleft")
|
||||
(List(
|
||||
pipe_(
|
||||
m_ap,
|
||||
fmap(pipe(
|
||||
AdventurePointsCategories.A.available,
|
||||
signNeg
|
||||
)),
|
||||
fromMaybe <string | number >("")
|
||||
)
|
||||
))}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
))
|
||||
(m_ap)}
|
||||
<TooltipToggle
|
||||
position="bottom"
|
||||
margin={12}
|
||||
content={
|
||||
<AdventurePointsTooltip />
|
||||
}
|
||||
target={
|
||||
<div className="collected-ap">
|
||||
{translate("header.apleft", available)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
icon=""
|
||||
onClick={undo}
|
||||
disabled={!isUndoAvailable}
|
||||
label={translate("Undo")}
|
||||
// onClick={undo}
|
||||
disabled/* ={!isUndoAvailable} */
|
||||
/>
|
||||
<IconButton
|
||||
icon=""
|
||||
onClick={redo}
|
||||
disabled={!isRedoAvailable}
|
||||
/>
|
||||
<BorderButton
|
||||
label={translate(staticData)("header.savebtn")}
|
||||
onClick={saveHero}
|
||||
label={translate("Redo")}
|
||||
// onClick={redo}
|
||||
disabled/* ={!isRedoAvailable} */
|
||||
/>
|
||||
<Button /* onClick={saveHero} */>
|
||||
{translate("Save")}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
: null} */}
|
||||
{/* <IconButton
|
||||
: null}
|
||||
<IconButton
|
||||
icon=""
|
||||
onClick={openSettings}
|
||||
label={translate("Show Settings")}
|
||||
// onClick={openSettings}
|
||||
disabled
|
||||
/>
|
||||
<SettingsContainer
|
||||
{/* <SettingsContainer
|
||||
staticData={staticData}
|
||||
isSettingsOpen={isSettingsOpen}
|
||||
close={closeSettings}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
min-width: 0;
|
||||
z-index: 2;
|
||||
|
||||
&::before,
|
||||
&::marker {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.tab--active {
|
||||
pointer-events: none;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FC } from "react"
|
||||
import backgroundImg from "../../assets/images/background.svg"
|
||||
import { TitleBar } from "../../shared/components/titleBar/TitleBar.tsx"
|
||||
import { Theme } from "../../shared/schema/config.ts"
|
||||
import { classList } from "../../shared/utils/classList.ts"
|
||||
import { ExternalAPI } from "../external.ts"
|
||||
import { useAppSelector } from "../hooks/redux.ts"
|
||||
import { selectAreAnimationsEnabled, selectLocale, selectTheme } from "../slices/settingsSlice.ts"
|
||||
import { NavigationBar } from "./NavigationBar.tsx"
|
||||
import "./Root.scss"
|
||||
import { Router } from "./Router.tsx"
|
||||
@@ -14,16 +15,15 @@ const handleRestore = ExternalAPI.restore
|
||||
const handleClose = ExternalAPI.close
|
||||
|
||||
export const Root: FC = () => {
|
||||
const theme = Theme.Dark
|
||||
const areAnimationsEnabled = true
|
||||
const language = "de-DE"
|
||||
const theme = useAppSelector(selectTheme)
|
||||
const language = useAppSelector(selectLocale)
|
||||
const areAnimationsEnabled = useAppSelector(selectAreAnimationsEnabled)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="body"
|
||||
className={classList(
|
||||
`theme-${theme}`,
|
||||
`platform-${ExternalAPI.platform}`,
|
||||
`theme--${theme}`,
|
||||
{ "show-animations": areAnimationsEnabled },
|
||||
)}
|
||||
lang={language}
|
||||
|
||||
@@ -6,6 +6,7 @@ import "./Router.scss"
|
||||
import { Imprint } from "./about/Imprint.tsx"
|
||||
import { LastChanges } from "./about/LastChanges.tsx"
|
||||
import { ThirdPartyLicenses } from "./about/ThirdPartyLicenses.tsx"
|
||||
import { Attributes } from "./characters/character/attributes/Attributes.tsx"
|
||||
|
||||
export const Router: FC = () => {
|
||||
const route = useAppSelector(selectRoute)
|
||||
@@ -30,7 +31,7 @@ export const Router: FC = () => {
|
||||
case "culture": return null // unwrapWithHero(hero => ( <CulturesContainer staticData={staticData} hero={hero} /> )),
|
||||
case "profession": return null // unwrapWithHero(hero => ( <ProfessionsContainer staticData={staticData} hero={hero} /> )),
|
||||
|
||||
case "attributes": return null // unwrapWithHero(hero => ( <AttributesContainer staticData={staticData} hero={hero} /> )),
|
||||
case "attributes": return <Attributes /> // unwrapWithHero(hero => ( <AttributesContainer staticData={staticData} hero={hero} /> )),
|
||||
|
||||
case "advantages": return null // unwrapWithHero(hero => ( <AdvantagesContainer staticData={staticData} hero={hero} /> )),
|
||||
case "disadvantages": return null // unwrapWithHero(hero => ( <DisadvantagesContainer staticData={staticData} hero={hero} /> )),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#attributes {
|
||||
.attribute-adjustment {
|
||||
margin: 10px 0 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> span::after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
> .dropdown {
|
||||
margin: 0 0 0 16px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// import * as React from "react"
|
||||
// import { elem, flength, List } from "../../../Data/List"
|
||||
// import { fromMaybe, isNothing, joinMaybeList, Just, liftM2, mapMaybe, Maybe, Nothing } from "../../../Data/Maybe"
|
||||
// import { Record } from "../../../Data/Record"
|
||||
// import { AttributeWithRequirements, AttributeWithRequirementsA_ } from "../../Models/View/AttributeWithRequirements"
|
||||
// import { DropdownOption } from "../../Models/View/DropdownOption"
|
||||
// import { StaticDataRecord } from "../../Models/Wiki/WikiModel"
|
||||
// import { translate } from "../../Utilities/I18n"
|
||||
// import { sign } from "../../Utilities/NumberUtils"
|
||||
// import { pipe_ } from "../../Utilities/pipe"
|
||||
// import { Dropdown } from "../Universal/Dropdown"
|
||||
import "./AttributeAdjustment.scss"
|
||||
|
||||
// export interface AttributesAdjustmentProps {
|
||||
// adjustmentValue: Maybe<number>
|
||||
// attributes: Maybe<List<Record<AttributeWithRequirements>>>
|
||||
// availableAttributeIds: Maybe<List<string>>
|
||||
// currentAttributeId: Maybe<string>
|
||||
// staticData: StaticDataRecord
|
||||
// setAdjustmentId (id: Maybe<string>): void
|
||||
// }
|
||||
|
||||
// const AWRA_ = AttributeWithRequirementsA_
|
||||
|
||||
// export const AttributesAdjustment: FC<AttributesAdjustmentProps> = props => {
|
||||
// const {
|
||||
// attributes: mattributes,
|
||||
// currentAttributeId,
|
||||
// adjustmentValue: madjustment,
|
||||
// availableAttributeIds: mavailable_attr_ids,
|
||||
// setAdjustmentId,
|
||||
// } = props
|
||||
|
||||
// const translate = useTranslate()
|
||||
|
||||
// return (
|
||||
// <div className="attribute-adjustment">
|
||||
// <span className="label">
|
||||
// {translate("attributes.attributeadjustmentselection")}
|
||||
// </span>
|
||||
// {fromMaybe
|
||||
// (<></>)
|
||||
// (liftM2((available_attr_ids: List<string>) => (adjustment: number) => (
|
||||
// <Dropdown
|
||||
// options={
|
||||
// pipe_(
|
||||
// mattributes,
|
||||
// joinMaybeList,
|
||||
// mapMaybe(x => elem(AWRA_.id(x))(available_attr_ids)
|
||||
// ? Just(DropdownOption({
|
||||
// id: Just(AWRA_.id(x)),
|
||||
// name: `${AWRA_.name(x)} ${sign(adjustment)}`,
|
||||
// }))
|
||||
// : Nothing)
|
||||
// )
|
||||
// }
|
||||
// value={currentAttributeId}
|
||||
// onChange={setAdjustmentId}
|
||||
// disabled={isNothing(currentAttributeId) || flength(available_attr_ids) === 1}
|
||||
// />
|
||||
// ))
|
||||
// (mavailable_attr_ids)
|
||||
// (madjustment))}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
+11
-9
@@ -1,10 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { List } from "../../../Data/List"
|
||||
import { Just, Maybe } from "../../../Data/Maybe"
|
||||
import { classListMaybe } from "../../Utilities/CSS"
|
||||
import { TooltipToggle } from "../Universal/TooltipToggle"
|
||||
import { FC } from "react"
|
||||
import { TooltipToggle } from "../../../../../shared/components/tooltipToggle/TooltipToggle.tsx"
|
||||
import { classList } from "../../../../../shared/utils/classList.ts"
|
||||
|
||||
export interface AttributeBorderProps {
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string
|
||||
@@ -13,13 +11,17 @@ export interface AttributeBorderProps {
|
||||
value: number | string
|
||||
}
|
||||
|
||||
export const AttributeBorder: React.FC<AttributeBorderProps> = props => {
|
||||
export const AttributeBorder: FC<Props> = props => {
|
||||
const { children, className, label, tooltip, tooltipMargin, value } = props
|
||||
|
||||
const valueElement =
|
||||
tooltip === undefined
|
||||
? (
|
||||
<div className="value"><div className="value-inner"><div>{value}</div></div></div>
|
||||
<div className="value">
|
||||
<div className="value-inner">
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<TooltipToggle
|
||||
@@ -36,7 +38,7 @@ export const AttributeBorder: React.FC<AttributeBorderProps> = props => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classListMaybe (List (Just ("attr"), Maybe (className)))}>
|
||||
<div className={classList("attr", className)}>
|
||||
<div className="short">{label}</div>
|
||||
{valueElement}
|
||||
{children}
|
||||
@@ -0,0 +1,161 @@
|
||||
#attributes {
|
||||
.scroll-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.counter {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.short {
|
||||
font: bold 14px/28px Alegreya;
|
||||
color: var(--headings-color);
|
||||
letter-spacing: 0.05em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value-inner {
|
||||
border: 1px solid var(--separator-color-transparent);
|
||||
padding: 1px;
|
||||
|
||||
> div {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
font: bold 28px/45px Alegreya Sans;
|
||||
font-variant-numeric: lining-nums;
|
||||
color: var(--headings-color);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.btn--round {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.attr {
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
position: relative;
|
||||
|
||||
.number-box {
|
||||
top: 70px;
|
||||
right: -6px;
|
||||
width: 24px;
|
||||
z-index: 2;
|
||||
|
||||
+ .btn--round {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
border: 2px solid transparent;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
&.attr--1 > .value {
|
||||
border-color: var(--courage-color);
|
||||
}
|
||||
|
||||
&.attr--2 > .value {
|
||||
border-color: var(--sagacity-color);
|
||||
}
|
||||
|
||||
&.attr--3 > .value {
|
||||
border-color: var(--intuition-color);
|
||||
}
|
||||
|
||||
&.attr--4 > .value {
|
||||
border-color: var(--charisma-color);
|
||||
}
|
||||
|
||||
&.attr--5 > .value {
|
||||
border-color: var(--dexterity-color);
|
||||
}
|
||||
|
||||
&.attr--6 > .value {
|
||||
border-color: var(--agility-color);
|
||||
}
|
||||
|
||||
&.attr--7 > .value {
|
||||
border-color: var(--constitution-color);
|
||||
}
|
||||
|
||||
&.attr--8 > .value {
|
||||
border-color: var(--strength-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay > .calc-attr-overlay {
|
||||
width: 280px;
|
||||
|
||||
p.calc-text {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.permanent-points-editor {
|
||||
.modal-header {
|
||||
padding: 16px 0;
|
||||
|
||||
.modal-header-inner {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1 1 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-bottom: 10px;
|
||||
|
||||
&:nth-child(2) {
|
||||
margin-left: 1px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: var(--separator-color-transparent);
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font: bold 40px/1 Alegreya;
|
||||
color: var(--headings-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
|
||||
.remove {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FC } from "react"
|
||||
import { Page } from "../../../../../shared/components/page/Page.tsx"
|
||||
import { Scroll } from "../../../../../shared/components/scroll/Scroll.tsx"
|
||||
import { useAppSelector } from "../../../../hooks/redux.ts"
|
||||
import { useTranslate } from "../../../../hooks/translate.ts"
|
||||
import { selectTotalPoints } from "../../../../selectors/attributeSelectors.ts"
|
||||
import { selectIsInCharacterCreation } from "../../../../selectors/characterSelectors.ts"
|
||||
import { selectMaximumTotalAttributePoints } from "../../../../selectors/experienceLevelSelectors.ts"
|
||||
import { selectAttributes } from "../../../../slices/characterSlice.ts"
|
||||
import "./Attributes.scss"
|
||||
import { AttributeList } from "./AttributesList.tsx"
|
||||
import { DerivedCharacteristicsList } from "./DerivedCharacteristicsList.tsx"
|
||||
|
||||
export const Attributes: FC = () => {
|
||||
// TODO: Replace with actual selectors
|
||||
const isRemovingEnabled = true
|
||||
|
||||
const translate = useTranslate()
|
||||
const totalPoints = useAppSelector(selectTotalPoints)
|
||||
const maxTotalPoints = useAppSelector(selectMaximumTotalAttributePoints)
|
||||
const isInCharacterCreation = useAppSelector(selectIsInCharacterCreation)
|
||||
|
||||
console.log(useAppSelector(selectAttributes))
|
||||
|
||||
return (
|
||||
<Page id="attributes">
|
||||
<Scroll>
|
||||
<div className="counter">
|
||||
{translate("Total Points")}
|
||||
{": "}
|
||||
{isInCharacterCreation ? `${totalPoints} / ${maxTotalPoints}` : totalPoints}
|
||||
</div>
|
||||
<AttributeList
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
/>
|
||||
<div className="secondary">
|
||||
{/* {isInCharacterCreation
|
||||
? (
|
||||
<AttributesAdjustment
|
||||
adjustmentValue={adjustmentValue}
|
||||
attributes={attributes}
|
||||
availableAttributeIds={availableAttributeIds}
|
||||
currentAttributeId={currentAttributeId}
|
||||
staticData={staticData}
|
||||
setAdjustmentId={setAdjustmentId}
|
||||
/>
|
||||
)
|
||||
: null} */}
|
||||
<DerivedCharacteristicsList
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
/>
|
||||
</div>
|
||||
</Scroll>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#attributes {
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FC } from "react"
|
||||
import { useAppSelector } from "../../../../hooks/redux.ts"
|
||||
import { selectVisibleAttributes } from "../../../../selectors/attributeSelectors.ts"
|
||||
import "./AttributesList.scss"
|
||||
import { AttributeListItem } from "./AttributesListItem.tsx"
|
||||
|
||||
type Props = {
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
}
|
||||
|
||||
export const AttributeList: FC<Props> = props => {
|
||||
const attributes = useAppSelector(selectVisibleAttributes)
|
||||
|
||||
const {
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="main">
|
||||
{attributes.map(attribute => (
|
||||
<AttributeListItem
|
||||
key={attribute.static.id}
|
||||
attribute={attribute}
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FC, useCallback } from "react"
|
||||
import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx"
|
||||
import { NumberBox } from "../../../../../shared/components/numberBox/NumberBox.tsx"
|
||||
import { useAppDispatch } from "../../../../hooks/redux.ts"
|
||||
import { useTranslate } from "../../../../hooks/translate.ts"
|
||||
import { useTranslateMap } from "../../../../hooks/translateMap.ts"
|
||||
import { DisplayedAttribute } from "../../../../selectors/attributeSelectors.ts"
|
||||
import { decrementAttribute, incrementAttribute } from "../../../../slices/attributesSlice.ts"
|
||||
import { AttributeBorder } from "./AttributeBorder.tsx"
|
||||
|
||||
type Props = {
|
||||
attribute: DisplayedAttribute
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
}
|
||||
|
||||
export const AttributeListItem: FC<Props> = props => {
|
||||
const {
|
||||
attribute,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
} = props
|
||||
|
||||
const translate = useTranslate()
|
||||
const translateMap = useTranslateMap()
|
||||
const translations = translateMap(attribute.static.translations)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const {
|
||||
dynamic: { value },
|
||||
static: { id },
|
||||
maximum: max,
|
||||
isDecreasable,
|
||||
isIncreasable,
|
||||
} = attribute
|
||||
|
||||
const valueHeader = isInCharacterCreation ? `${value} / ${max}` : value
|
||||
|
||||
const handleAdd = useCallback(
|
||||
() => dispatch(incrementAttribute(id)),
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const handleRemove = useCallback(
|
||||
() => dispatch(decrementAttribute(id)),
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
return (
|
||||
<AttributeBorder
|
||||
className={`attr--${id.toFixed()}`}
|
||||
label={translations?.abbreviation ?? id.toFixed()}
|
||||
value={value}
|
||||
tooltip={
|
||||
<div className="calc-attr-overlay">
|
||||
<h4>
|
||||
<span>{translations?.name ?? id.toFixed()}</span>
|
||||
<span>{valueHeader}</span>
|
||||
</h4>
|
||||
{
|
||||
translations === undefined
|
||||
? null
|
||||
: <p>{translations.description}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
tooltipMargin={11}
|
||||
>
|
||||
{isInCharacterCreation ? <NumberBox max={max} /> : null}
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
onClick={handleAdd}
|
||||
disabled={!isIncreasable}
|
||||
label={translate("Increment")}
|
||||
/>
|
||||
{isRemovingEnabled
|
||||
? (
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
onClick={handleRemove}
|
||||
disabled={!isDecreasable}
|
||||
label={translate("Decrement")}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</AttributeBorder>
|
||||
)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#attributes {
|
||||
.derived-characteristics {
|
||||
display: flex;
|
||||
|
||||
.value {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.number-box {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.derived-characteristics-item {
|
||||
}
|
||||
|
||||
.permanent {
|
||||
.value-inner > div {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.number-box {
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FC } from "react"
|
||||
import { useAppSelector } from "../../../../hooks/redux.ts"
|
||||
import { selectDerivedCharacteristics } from "../../../../selectors/derivedCharacteristicsSelectors.ts"
|
||||
import "./DerivedCharacteristicsList.scss"
|
||||
import { DerivedCharacteristicsListItem } from "./DerivedCharacteristicsListItem.tsx"
|
||||
|
||||
type Props = {
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
}
|
||||
|
||||
export const DerivedCharacteristicsList: FC<Props> = props => {
|
||||
const {
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
} = props
|
||||
|
||||
const derivedCharacteristics = useAppSelector(selectDerivedCharacteristics)
|
||||
|
||||
return (
|
||||
<div className="derived-characteristics">
|
||||
{derivedCharacteristics.map(derivedCharacteristic => (
|
||||
<DerivedCharacteristicsListItem
|
||||
key={derivedCharacteristic.id}
|
||||
attribute={derivedCharacteristic}
|
||||
isInCharacterCreation={isInCharacterCreation}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import { FC, useCallback } from "react"
|
||||
import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx"
|
||||
import { NumberBox } from "../../../../../shared/components/numberBox/NumberBox.tsx"
|
||||
import { DerivedCharacteristicIdentifier as DCId } from "../../../../../shared/domain/identifier.ts"
|
||||
import { sign } from "../../../../../shared/utils/math.ts"
|
||||
import { useAppDispatch } from "../../../../hooks/redux.ts"
|
||||
import { useTranslate } from "../../../../hooks/translate.ts"
|
||||
import { useTranslateMap } from "../../../../hooks/translateMap.ts"
|
||||
import { DisplayedDerivedCharacteristic, isDisplayedEnergy } from "../../../../selectors/derivedCharacteristicsSelectors.ts"
|
||||
import { decrementArcaneEnergy, decrementKarmaPoints, decrementLifePoints, incrementArcaneEnergy, incrementKarmaPoints, incrementLifePoints } from "../../../../slices/derivedCharacteristicsSlice.ts"
|
||||
import { AttributeBorder } from "./AttributeBorder.tsx"
|
||||
import { DerivedCharacteristicsListItemPermanent } from "./DerivedCharacteristicsListItemPermanent.tsx"
|
||||
|
||||
type Props = {
|
||||
attribute: DisplayedDerivedCharacteristic
|
||||
isInCharacterCreation: boolean
|
||||
isRemovingEnabled: boolean
|
||||
}
|
||||
|
||||
export const DerivedCharacteristicsListItem: FC<Props> = props => {
|
||||
const {
|
||||
attribute,
|
||||
isInCharacterCreation,
|
||||
isRemovingEnabled,
|
||||
} = props
|
||||
|
||||
const {
|
||||
id,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
purchaseMaximum,
|
||||
purchased,
|
||||
permanentlyLost,
|
||||
permanentlyLostBoughtBack,
|
||||
} = attribute
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const translate = useTranslate()
|
||||
const translateMap = useTranslateMap()
|
||||
const translations = translateMap(attribute.static.translations)
|
||||
|
||||
const handleAddMaxEnergyPoint = useCallback(
|
||||
() => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: dispatch(incrementLifePoints); break
|
||||
case DCId.ArcaneEnergy: dispatch(incrementArcaneEnergy); break
|
||||
case DCId.KarmaPoints: dispatch(incrementKarmaPoints); break
|
||||
default: break
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const handleRemoveMaxEnergyPoint = useCallback(
|
||||
() => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: dispatch(decrementLifePoints); break
|
||||
case DCId.ArcaneEnergy: dispatch(decrementArcaneEnergy); break
|
||||
case DCId.KarmaPoints: dispatch(decrementKarmaPoints); break
|
||||
default: break
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="derived-characteristics-item">
|
||||
<AttributeBorder
|
||||
label={translations?.abbreviation ?? ""}
|
||||
value={value}
|
||||
tooltip={(
|
||||
<div className="calc-attr-overlay">
|
||||
<h4>
|
||||
<span>{translations?.name ?? ""}</span>
|
||||
<span>{value}</span>
|
||||
</h4>
|
||||
<p className="calc-text">
|
||||
{translations?.calculation?.default ?? ""}
|
||||
{" = "}
|
||||
{base}
|
||||
</p>
|
||||
<p>
|
||||
<span className="mod">
|
||||
{translate("Modifier")}
|
||||
{": "}
|
||||
{sign(modifier)}
|
||||
<br />
|
||||
</span>
|
||||
{purchased !== undefined && !isInCharacterCreation
|
||||
? (
|
||||
<span className="add">
|
||||
{translate("Bought")}
|
||||
{": "}
|
||||
{purchased}
|
||||
{" / "}
|
||||
{purchaseMaximum}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
tooltipMargin={7}
|
||||
>
|
||||
{purchaseMaximum !== undefined && purchaseMaximum > 0 && !isInCharacterCreation
|
||||
? <NumberBox current={purchased} max={purchaseMaximum} />
|
||||
: null}
|
||||
{
|
||||
!isInCharacterCreation
|
||||
&& purchased !== undefined
|
||||
&& purchaseMaximum !== undefined
|
||||
? (
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
label={translate("Increment")}
|
||||
onClick={handleAddMaxEnergyPoint}
|
||||
disabled={
|
||||
purchased >= purchaseMaximum
|
||||
|| (
|
||||
id !== DCId.LifePoints
|
||||
&& permanentlyLost !== undefined
|
||||
&& permanentlyLostBoughtBack !== undefined
|
||||
&& permanentlyLost >= permanentlyLostBoughtBack
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{
|
||||
!isInCharacterCreation
|
||||
&& isRemovingEnabled
|
||||
&& purchased !== undefined
|
||||
&& purchaseMaximum !== undefined
|
||||
? (
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
label={translate("Decrement")}
|
||||
onClick={handleRemoveMaxEnergyPoint}
|
||||
disabled={
|
||||
purchased <= 0
|
||||
|| (
|
||||
id !== DCId.LifePoints
|
||||
&& permanentlyLost !== undefined
|
||||
&& permanentlyLostBoughtBack !== undefined
|
||||
&& permanentlyLost >= permanentlyLostBoughtBack
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</AttributeBorder>
|
||||
{
|
||||
isDisplayedEnergy(attribute)
|
||||
? (
|
||||
<DerivedCharacteristicsListItemPermanent
|
||||
attribute={attribute}
|
||||
isRemovingEnabled={isRemovingEnabled}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
import { FC, useCallback } from "react"
|
||||
import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx"
|
||||
import { DerivedCharacteristicIdentifier as DCId } from "../../../../../shared/domain/identifier.ts"
|
||||
import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts"
|
||||
import { useModalState } from "../../../../hooks/modalState.ts"
|
||||
import { useAppDispatch } from "../../../../hooks/redux.ts"
|
||||
import { useTranslate } from "../../../../hooks/translate.ts"
|
||||
import { DisplayedDerivedCharacteristic } from "../../../../selectors/derivedCharacteristicsSelectors.ts"
|
||||
import { addArcaneEnergyPermanentlyLost, addKarmaPointsPermanentlyLost, addLifePointsPermanentlyLost, decrementArcaneEnergyBoughtBack, decrementArcaneEnergyPermanentlyLost, decrementKarmaPointsBoughtBack, decrementKarmaPointsPermanentlyLost, decrementLifePointsPermanentlyLost, incrementArcaneEnergyBoughtBack, incrementArcaneEnergyPermanentlyLost, incrementKarmaPointsBoughtBack, incrementKarmaPointsPermanentlyLost, incrementLifePointsPermanentlyLost } from "../../../../slices/derivedCharacteristicsSlice.ts"
|
||||
import { AttributeBorder } from "./AttributeBorder.tsx"
|
||||
import { PermanentLossSheet } from "./PermanentLossSheet.tsx"
|
||||
import { PermanentPointsSheet } from "./PermanentPointsSheet.tsx"
|
||||
|
||||
type Props = {
|
||||
attribute: DisplayedDerivedCharacteristic<DCId.LifePoints | DCId.ArcaneEnergy | DCId.KarmaPoints>
|
||||
isRemovingEnabled: boolean
|
||||
}
|
||||
|
||||
export const DerivedCharacteristicsListItemPermanent: FC<Props> = props => {
|
||||
const {
|
||||
attribute,
|
||||
isRemovingEnabled,
|
||||
} = props
|
||||
|
||||
const { id, permanentlyLost = 0, permanentlyLostBoughtBack } = attribute
|
||||
const dispatch = useAppDispatch()
|
||||
const translate = useTranslate()
|
||||
|
||||
const {
|
||||
isOpen: isPermanentPointsSheetOpen,
|
||||
open: openPermanentPointsSheet,
|
||||
close: closePermanentPointsSheet,
|
||||
} = useModalState()
|
||||
|
||||
const {
|
||||
isOpen: isPermanentLossSheetOpen,
|
||||
open: openPermanentLossSheet,
|
||||
close: closePermanentLossSheet,
|
||||
} = useModalState()
|
||||
|
||||
const handleAddPermanentlyLostPoint = useCallback(
|
||||
() => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: dispatch(incrementLifePointsPermanentlyLost()); break
|
||||
case DCId.ArcaneEnergy: dispatch(incrementArcaneEnergyPermanentlyLost()); break
|
||||
case DCId.KarmaPoints: dispatch(incrementKarmaPointsPermanentlyLost()); break
|
||||
default: assertExhaustive(id)
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const handleAddPermanentlyLostPoints = useCallback(
|
||||
(value: number) => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: dispatch(addLifePointsPermanentlyLost(value)); break
|
||||
case DCId.ArcaneEnergy: dispatch(addArcaneEnergyPermanentlyLost(value)); break
|
||||
case DCId.KarmaPoints: dispatch(addKarmaPointsPermanentlyLost(value)); break
|
||||
default: assertExhaustive(id)
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const handleRemovePermanentlyLostPoint = useCallback(
|
||||
() => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: dispatch(decrementLifePointsPermanentlyLost()); break
|
||||
case DCId.ArcaneEnergy: dispatch(decrementArcaneEnergyPermanentlyLost()); break
|
||||
case DCId.KarmaPoints: dispatch(decrementKarmaPointsPermanentlyLost()); break
|
||||
default: assertExhaustive(id)
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const handleAddBoughtBackPoint = useCallback(
|
||||
() => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: break
|
||||
case DCId.ArcaneEnergy: dispatch(incrementArcaneEnergyBoughtBack()); break
|
||||
case DCId.KarmaPoints: dispatch(incrementKarmaPointsBoughtBack()); break
|
||||
default: assertExhaustive(id)
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const handleRemoveBoughtBackPoint = useCallback(
|
||||
() => {
|
||||
switch (id) {
|
||||
case DCId.LifePoints: break
|
||||
case DCId.ArcaneEnergy: dispatch(decrementArcaneEnergyBoughtBack); break
|
||||
case DCId.KarmaPoints: dispatch(decrementKarmaPointsBoughtBack); break
|
||||
default: assertExhaustive(id)
|
||||
}
|
||||
},
|
||||
[ dispatch, id ]
|
||||
)
|
||||
|
||||
const available = typeof permanentlyLostBoughtBack === "number"
|
||||
? permanentlyLost - permanentlyLostBoughtBack
|
||||
: permanentlyLost
|
||||
|
||||
const [ label, name ] =
|
||||
id === DCId.LifePoints
|
||||
? [
|
||||
translate("pLP"),
|
||||
translate("Permanently Lost Life Points"),
|
||||
]
|
||||
: id === DCId.ArcaneEnergy
|
||||
? [
|
||||
translate("pAE"),
|
||||
translate("Permanently Lost Arcane Energy"),
|
||||
]
|
||||
: [
|
||||
translate("pKP"),
|
||||
translate("Permanently Lost Karma Points"),
|
||||
]
|
||||
|
||||
return (
|
||||
<AttributeBorder
|
||||
className="permanent"
|
||||
label={label}
|
||||
value={available}
|
||||
tooltip={
|
||||
<div className="calc-attr-overlay">
|
||||
<h4>
|
||||
<span>{name}</span>
|
||||
<span>{available}</span>
|
||||
</h4>
|
||||
{
|
||||
typeof permanentlyLostBoughtBack === "number"
|
||||
? (
|
||||
<p>
|
||||
{translate("Lost Total")}
|
||||
{": "}
|
||||
{permanentlyLost}
|
||||
<br />
|
||||
{translate("Bought Back")}
|
||||
{": "}
|
||||
{permanentlyLostBoughtBack}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p>
|
||||
{translate("Lost Total")}
|
||||
{": "}
|
||||
{permanentlyLost}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
tooltipMargin={7}
|
||||
>
|
||||
{isRemovingEnabled
|
||||
? (
|
||||
<>
|
||||
<IconButton
|
||||
className="edit"
|
||||
icon=""
|
||||
label={translate("Lost Total")}
|
||||
onClick={openPermanentPointsSheet}
|
||||
/>
|
||||
<PermanentPointsSheet
|
||||
id={id}
|
||||
isOpen={isPermanentPointsSheetOpen}
|
||||
permanentlyLost={permanentlyLost}
|
||||
permanentlyLostBoughtBack={permanentlyLostBoughtBack}
|
||||
addBoughtBackPoint={handleAddBoughtBackPoint}
|
||||
addLostPoint={handleAddPermanentlyLostPoint}
|
||||
removeBoughtBackPoint={handleRemoveBoughtBackPoint}
|
||||
removeLostPoint={handleRemovePermanentlyLostPoint}
|
||||
close={closePermanentPointsSheet}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
label={translate("Lost Total")}
|
||||
onClick={openPermanentLossSheet}
|
||||
/>
|
||||
<PermanentLossSheet
|
||||
remove={handleAddPermanentlyLostPoints}
|
||||
isOpen={isPermanentLossSheetOpen}
|
||||
close={closePermanentLossSheet}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isRemovingEnabled && id !== DCId.LifePoints
|
||||
? (
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
label={translate("Buy Back Permanently Lost Point")}
|
||||
onClick={handleAddBoughtBackPoint}
|
||||
disabled={available <= 0}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</AttributeBorder>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FC, useCallback, useState } from "react"
|
||||
import { BasicInputDialog } from "../../../../../shared/components/basicInputDialog/BasicInputDialog.tsx"
|
||||
import { parseInt } from "../../../../../shared/utils/math.ts"
|
||||
import { isNaturalNumber } from "../../../../../shared/utils/regex.ts"
|
||||
import { useTranslate } from "../../../../hooks/translate.ts"
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
close(): void
|
||||
remove(value: number): void
|
||||
}
|
||||
|
||||
export const PermanentLossSheet: FC<Props> = props => {
|
||||
const { remove, isOpen, close } = props
|
||||
|
||||
const [ value, setValue ] = useState("")
|
||||
|
||||
const handleRemove = useCallback(
|
||||
() => {
|
||||
const parsedValue = parseInt(value)
|
||||
|
||||
if (parsedValue !== undefined) {
|
||||
remove(parsedValue)
|
||||
}
|
||||
},
|
||||
[ remove, value ]
|
||||
)
|
||||
|
||||
const translate = useTranslate()
|
||||
|
||||
return (
|
||||
<BasicInputDialog
|
||||
id="overview-add-ap"
|
||||
isOpen={isOpen}
|
||||
title={translate("attributes.removeenergypointslostpermanently.message")}
|
||||
description=""
|
||||
value={value}
|
||||
invalid={isNaturalNumber(value) ? undefined : ""}
|
||||
acceptLabel={translate("attributes.removeenergypointslostpermanently.removebtn")}
|
||||
rejectLabel={translate("general.dialogs.cancelbtn")}
|
||||
onClose={close}
|
||||
onAccept={handleRemove}
|
||||
onChange={setValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { FC } from "react"
|
||||
import { Dialog } from "../../../../../shared/components/dialog/Dialog.tsx"
|
||||
import { IconButton } from "../../../../../shared/components/iconButton/IconButton.tsx"
|
||||
import { DerivedCharacteristicIdentifier as DCId, EnergyIdentifier } from "../../../../../shared/domain/identifier.ts"
|
||||
import { useTranslate } from "../../../../hooks/translate.ts"
|
||||
|
||||
type Props = {
|
||||
id: EnergyIdentifier
|
||||
isOpen: boolean
|
||||
permanentlyLost: number
|
||||
permanentlyLostBoughtBack: number | undefined
|
||||
addBoughtBackPoint? (): void
|
||||
addLostPoint (): void
|
||||
removeBoughtBackPoint? (): void
|
||||
removeLostPoint (): void
|
||||
close (): void
|
||||
}
|
||||
|
||||
export const PermanentPointsSheet: FC<Props> = props => {
|
||||
const {
|
||||
id,
|
||||
isOpen,
|
||||
permanentlyLost,
|
||||
permanentlyLostBoughtBack,
|
||||
addBoughtBackPoint,
|
||||
addLostPoint,
|
||||
removeBoughtBackPoint,
|
||||
removeLostPoint,
|
||||
close,
|
||||
} = props
|
||||
|
||||
const translate = useTranslate()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id={id.toFixed()}
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
className="permanent-points-editor"
|
||||
title={
|
||||
id === DCId.ArcaneEnergy
|
||||
? translate("Permanently Lost Arcane Energy")
|
||||
: id === DCId.KarmaPoints
|
||||
? translate("Permanently Lost Karma Points")
|
||||
: translate("Permanently Lost Life Points")
|
||||
}
|
||||
buttons={[
|
||||
{
|
||||
autoWidth: true,
|
||||
label: translate("Done"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="main">
|
||||
{
|
||||
addBoughtBackPoint !== undefined
|
||||
&& removeBoughtBackPoint !== undefined
|
||||
&& permanentlyLostBoughtBack !== undefined
|
||||
? (
|
||||
<div className="column boughtback">
|
||||
<div className="value">{permanentlyLostBoughtBack}</div>
|
||||
<div className="description smallcaps">
|
||||
{translate("Bought Back")}
|
||||
</div>
|
||||
<div className="buttons">
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
label={translate("Increment")}
|
||||
onClick={addBoughtBackPoint}
|
||||
disabled={permanentlyLostBoughtBack >= permanentlyLost}
|
||||
/>
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
label={translate("Decrement")}
|
||||
onClick={removeBoughtBackPoint}
|
||||
disabled={permanentlyLostBoughtBack <= 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<div className="column lost">
|
||||
<div className="value">{permanentlyLost}</div>
|
||||
<div className="description smallcaps">
|
||||
{translate("Permanently Spent")}
|
||||
</div>
|
||||
<div className="buttons">
|
||||
<IconButton
|
||||
className="add"
|
||||
icon=""
|
||||
label={translate("Increment")}
|
||||
onClick={addLostPoint}
|
||||
/>
|
||||
<IconButton
|
||||
className="remove"
|
||||
icon=""
|
||||
label={translate("Decrement")}
|
||||
onClick={removeLostPoint}
|
||||
disabled={permanentlyLost <= 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { ImprovementCost, adventurePointsForRange } from "../../shared/domain/adventurePoints/improvementCost.ts"
|
||||
import { selectAttributes, selectCurrentCharacter, selectDerivedCharacteristics, selectTotalAdventurePoints } from "../slices/characterSlice.ts"
|
||||
|
||||
export type SpentAdventurePoints = {
|
||||
general: number
|
||||
bound: number
|
||||
}
|
||||
|
||||
export const selectAdventurePointsSpentOnAttributes = createSelector(
|
||||
selectAttributes,
|
||||
(attributes): SpentAdventurePoints => Object.values(attributes).reduce(
|
||||
(acc, attribute) => ({
|
||||
general: acc.general + attribute.cachedAdventurePoints.general,
|
||||
bound: acc.bound + attribute.cachedAdventurePoints.bound,
|
||||
}),
|
||||
{ general: 0, bound: 0 }
|
||||
)
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnSkills = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnCombatTechniques = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnSpells = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnLiturgicalChants = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnCantrips = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnBlessings = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnAdvantages = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnMagicalAdvantages = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnBlessedAdvantages = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnDisadvantages = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnMagicalDisadvantages = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnBlessedDisadvantages = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
// export const getMagicalAdvantagesDisadvantagesAdventurePointsMaximum = createMaybeSelector(
|
||||
// getCurrentHeroPresent,
|
||||
// fmap(getDisAdvantagesSubtypeMax(true))
|
||||
// )
|
||||
|
||||
export const selectAdventurePointsSpentOnSpecialAbilities = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): SpentAdventurePoints => ({ general: 0, bound: 0 })
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnEnergies = createSelector(
|
||||
selectDerivedCharacteristics,
|
||||
(derivedCharacteristics): number =>
|
||||
[
|
||||
derivedCharacteristics.lifePoints.purchased,
|
||||
derivedCharacteristics.arcaneEnergy.purchased,
|
||||
derivedCharacteristics.karmaPoints.purchased,
|
||||
].reduce(
|
||||
(acc, purchased) => acc + adventurePointsForRange(ImprovementCost.D, 0, purchased),
|
||||
0
|
||||
)
|
||||
+ derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack * 2
|
||||
+ derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack * 2
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnRace = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): number => 0
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpentOnProfession = createSelector(
|
||||
selectCurrentCharacter,
|
||||
(): number | undefined => undefined
|
||||
)
|
||||
|
||||
export const selectAdventurePointsSpent = createSelector(
|
||||
selectAdventurePointsSpentOnAttributes,
|
||||
selectAdventurePointsSpentOnSkills,
|
||||
selectAdventurePointsSpentOnCombatTechniques,
|
||||
selectAdventurePointsSpentOnSpells,
|
||||
selectAdventurePointsSpentOnLiturgicalChants,
|
||||
selectAdventurePointsSpentOnCantrips,
|
||||
selectAdventurePointsSpentOnBlessings,
|
||||
selectAdventurePointsSpentOnAdvantages,
|
||||
selectAdventurePointsSpentOnMagicalAdvantages,
|
||||
selectAdventurePointsSpentOnBlessedAdvantages,
|
||||
selectAdventurePointsSpentOnDisadvantages,
|
||||
selectAdventurePointsSpentOnMagicalDisadvantages,
|
||||
selectAdventurePointsSpentOnBlessedDisadvantages,
|
||||
selectAdventurePointsSpentOnSpecialAbilities,
|
||||
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 }
|
||||
)
|
||||
)
|
||||
|
||||
export const selectAdventurePointsAvailable = createSelector(
|
||||
selectTotalAdventurePoints,
|
||||
selectAdventurePointsSpent,
|
||||
(totalAdventurePoints = 0, { general: spentAdventurePoints }) =>
|
||||
totalAdventurePoints - spentAdventurePoints
|
||||
)
|
||||
|
||||
// export const getHasCurrentNoAddedAP = createMaybeSelector (
|
||||
// getTotalAdventurePoints,
|
||||
// getStartEl,
|
||||
// (mtotal_ap, mel) =>
|
||||
// elem (true)
|
||||
// (liftM2<number, Record<ExperienceLevel>, boolean>
|
||||
// (totalAdventurePoints => experienceLevel =>
|
||||
// totalAdventurePoints === ExperienceLevel.A.ap (experienceLevel))
|
||||
// (mtotal_ap)
|
||||
// (mel))
|
||||
// )
|
||||
@@ -0,0 +1,412 @@
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { Attribute } from "optolith-database-schema/types/Attribute"
|
||||
import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel"
|
||||
import { Race } from "optolith-database-schema/types/Race"
|
||||
import { Rated } from "../../shared/domain/ratedEntry.ts"
|
||||
import { createDynamicAttribute } from "../slices/attributesSlice.ts"
|
||||
import { selectAttributeAdjustmentId, selectAttributes as selectDynamicAttributes } from "../slices/characterSlice.ts"
|
||||
import { selectAttributes as selectStaticAttributes } from "../slices/databaseSlice.ts"
|
||||
import { RootState } from "../store.ts"
|
||||
import { selectIsInCharacterCreation } from "./characterSelectors.ts"
|
||||
import { selectCurrentExperienceLevel, selectMaximumTotalAttributePoints, selectStartExperienceLevel } from "./experienceLevelSelectors.ts"
|
||||
import { selectCurrentRace } from "./raceSelectors.ts"
|
||||
|
||||
export type DisplayedAttribute = {
|
||||
static: Attribute
|
||||
dynamic: Rated
|
||||
minimum: number
|
||||
maximum?: number
|
||||
isIncreasable: boolean
|
||||
isDecreasable: boolean
|
||||
}
|
||||
|
||||
export const selectTotalPoints = createSelector(
|
||||
selectStaticAttributes,
|
||||
selectDynamicAttributes,
|
||||
(attributes, dynamicAttributes): number =>
|
||||
Object.values(attributes).reduce(
|
||||
(sum, { id }) => sum + (dynamicAttributes[id]?.value ?? 8),
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
const getMinimum = (): number => {
|
||||
// (wiki: StaticDataRecord) =>
|
||||
// (hero: HeroModelRecord) =>
|
||||
// /**
|
||||
// * `(lp, ae, kp)`
|
||||
// */
|
||||
// (added: Tuple<[number, number, number]>) =>
|
||||
// (mblessed_primary_attr: Maybe<Record<AttributeCombined>>) =>
|
||||
// (mhighest_magical_primary_attr: Maybe<Record<AttributeCombined>>) =>
|
||||
// (hero_entry: Record<AttributeDependent>): number => {
|
||||
|
||||
// const isConstitution = AtDA.id (hero_entry) === AttrId.Constitution
|
||||
|
||||
// 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 = [
|
||||
8,
|
||||
// ...flattenDependencies (wiki) (hero) (AtDA.dependencies (hero_entry)),
|
||||
// ...(isConstitution ? [ sel1 (added) ] : []),
|
||||
// ...(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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modifier if the attribute specified by `id` is a member of the
|
||||
* 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
|
||||
|
||||
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)
|
||||
|
||||
const getMaximum = (
|
||||
id: number,
|
||||
isInCharacterCreation: boolean,
|
||||
race: Race | undefined,
|
||||
startExperienceLevel: ExperienceLevel | undefined,
|
||||
currentExperienceLevel: ExperienceLevel | undefined,
|
||||
isAttributeValueLimitEnabled: boolean,
|
||||
adjustmentId: number | undefined,
|
||||
): number | undefined => {
|
||||
if (isInCharacterCreation && race !== undefined && startExperienceLevel !== undefined) {
|
||||
const selectedAdjustment = adjustmentId === id ? getModIfSelectedAdjustment(id, race) : 0
|
||||
const staticAdjustment = getModIfStaticAdjustment(id, race)
|
||||
|
||||
return startExperienceLevel.max_attribute_value + selectedAdjustment + staticAdjustment
|
||||
}
|
||||
|
||||
if (isAttributeValueLimitEnabled && currentExperienceLevel !== undefined) {
|
||||
return currentExperienceLevel.max_attribute_value + 2
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const isDecreasable = (
|
||||
dynamic: Rated,
|
||||
min: number,
|
||||
) => min < dynamic.value
|
||||
|
||||
const isIncreasable = (
|
||||
dynamic: Rated,
|
||||
max: number | undefined,
|
||||
totalPoints: number,
|
||||
maxTotalPoints: number,
|
||||
isInCharacterCreation: boolean,
|
||||
) =>
|
||||
(!isInCharacterCreation || totalPoints < maxTotalPoints)
|
||||
&& (max === undefined || dynamic.value < max)
|
||||
|
||||
export const selectVisibleAttributes = createSelector(
|
||||
selectStaticAttributes,
|
||||
selectDynamicAttributes,
|
||||
selectTotalPoints,
|
||||
selectMaximumTotalAttributePoints,
|
||||
selectIsInCharacterCreation,
|
||||
selectCurrentRace,
|
||||
selectStartExperienceLevel,
|
||||
selectCurrentExperienceLevel,
|
||||
// TODO: replace with selector
|
||||
(_state: RootState): boolean => false,
|
||||
selectAttributeAdjustmentId,
|
||||
(
|
||||
attributes,
|
||||
dynamicAttributes,
|
||||
totalPoints,
|
||||
maxTotalPoints,
|
||||
isInCharacterCreation,
|
||||
currentRace,
|
||||
startExperienceLevel,
|
||||
currentExperienceLevel,
|
||||
isAttributeValueLimitEnabled,
|
||||
attributeAdjustmentId,
|
||||
): DisplayedAttribute[] =>
|
||||
Object.values(attributes)
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map(attribute => {
|
||||
const dynamicAttribute =
|
||||
dynamicAttributes[attribute.id] ?? createDynamicAttribute(attribute.id)
|
||||
|
||||
const minimum = getMinimum()
|
||||
const maximum = getMaximum(
|
||||
attribute.id,
|
||||
isInCharacterCreation,
|
||||
currentRace,
|
||||
startExperienceLevel,
|
||||
currentExperienceLevel,
|
||||
isAttributeValueLimitEnabled,
|
||||
attributeAdjustmentId,
|
||||
)
|
||||
|
||||
return {
|
||||
static: attribute,
|
||||
dynamic: dynamicAttribute,
|
||||
minimum,
|
||||
maximum,
|
||||
isDecreasable:
|
||||
isDecreasable(
|
||||
dynamicAttribute,
|
||||
minimum
|
||||
),
|
||||
isIncreasable:
|
||||
isIncreasable(
|
||||
dynamicAttribute,
|
||||
maximum,
|
||||
totalPoints,
|
||||
maxTotalPoints,
|
||||
isInCharacterCreation,
|
||||
),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// const getAddedEnergies = createMaybeSelector (
|
||||
// getHeroProp,
|
||||
// hero => Tuple (
|
||||
// pipe_ (hero, HA.energies, EA.addedLifePoints),
|
||||
// pipe_ (hero, HA.energies, EA.addedArcaneEnergyPoints),
|
||||
// pipe_ (hero, HA.energies, EA.addedKarmaPoints)
|
||||
// )
|
||||
// )
|
||||
|
||||
// /**
|
||||
// * Returns the maximum attribute value of the list of given attribute ids.
|
||||
// */
|
||||
// export const getMaxAttributeValueByID =
|
||||
// (attributes: HeroModel["attributes"]) =>
|
||||
// pipe (
|
||||
// mapMaybe (pipe (lookupF (attributes), fmap (AtDA.value))),
|
||||
// consF (8),
|
||||
// maximum
|
||||
// )
|
||||
|
||||
// export const getPrimaryMagicalAttributes = createMaybeSelector (
|
||||
// getWikiAttributes,
|
||||
// getAttributes,
|
||||
// getMagicalTraditionsFromHero,
|
||||
// uncurryN3 (wiki_attributes =>
|
||||
// hero_attributes =>
|
||||
// mapMaybe (mapTradHeroEntryToAttrCombined (wiki_attributes) (hero_attributes)))
|
||||
// )
|
||||
|
||||
// export const getHighestPrimaryMagicalAttributeValue = createMaybeSelector (
|
||||
// getPrimaryMagicalAttributes,
|
||||
// pipe (ensure (notNull), fmap (List.foldr (pipe (ACA_.value, max)) (0)))
|
||||
// )
|
||||
|
||||
// export const getHighestPrimaryMagicalAttributes = createMaybeSelector (
|
||||
// getPrimaryMagicalAttributes,
|
||||
// getHighestPrimaryMagicalAttributeValue,
|
||||
// uncurryN (attrs => fmap (max_value => filter (pipe (ACA_.value, equals (max_value))) (attrs)))
|
||||
// )
|
||||
|
||||
// type AttrCs = List<Record<AttributeCombined>>
|
||||
// type NonEmptyAttrCs = NonEmptyList<Record<AttributeCombined>>
|
||||
|
||||
// export const getHighestPrimaryMagicalAttribute = createMaybeSelector (
|
||||
// getHighestPrimaryMagicalAttributes,
|
||||
// pipe (
|
||||
// bindF (ensure (pipe (flength, equals (1)) as (xs: AttrCs) => xs is NonEmptyAttrCs)),
|
||||
// fmap ((xs: NonEmptyAttrCs) => head (xs))
|
||||
// )
|
||||
// )
|
||||
|
||||
// export const getPrimaryMagicalAttributeForSheet = createMaybeSelector (
|
||||
// getPrimaryMagicalAttributes,
|
||||
// map (ACA_.short)
|
||||
// )
|
||||
|
||||
// export const getPrimaryBlessedAttribute = createMaybeSelector (
|
||||
// getBlessedTraditionFromState,
|
||||
// getAttributes,
|
||||
// getWikiAttributes,
|
||||
// (mtradition, hero_attributes, wiki_attributes) =>
|
||||
// bind (mtradition) (mapTradHeroEntryToAttrCombined (wiki_attributes) (hero_attributes))
|
||||
// )
|
||||
|
||||
// export const getPrimaryBlessedAttributeForSheet = createMaybeSelector (
|
||||
// getPrimaryBlessedAttribute,
|
||||
// fmap (pipe (ACA.wikiEntry, AA.short))
|
||||
// )
|
||||
|
||||
// /**
|
||||
// * Returns a `List` of attributes including state, full wiki infos and a
|
||||
// * minimum and optional maximum value.
|
||||
// */
|
||||
// export const getAttributesForView = createMaybeSelector (
|
||||
// getCurrentHeroPresent,
|
||||
// getStartEl,
|
||||
// getCurrentEl,
|
||||
// getCurrentPhase,
|
||||
// getAttributeValueLimit,
|
||||
// getWiki,
|
||||
// getRace,
|
||||
// getAddedEnergies,
|
||||
// getPrimaryBlessedAttribute,
|
||||
// getHighestPrimaryMagicalAttribute,
|
||||
// (
|
||||
// mhero,
|
||||
// startEl,
|
||||
// currentEl,
|
||||
// mphase,
|
||||
// attributeValueLimit,
|
||||
// wiki,
|
||||
// mrace,
|
||||
// added,
|
||||
// mblessed_primary_attr,
|
||||
// mhighest_magical_primary_attr
|
||||
// ) =>
|
||||
// fmapF (mhero)
|
||||
// (hero => foldr ((wiki_entry: Record<Attribute>) => {
|
||||
// const current_id = AA.id (wiki_entry)
|
||||
|
||||
// const hero_entry = fromMaybe (createPlainAttributeDependent (current_id))
|
||||
// (pipe_ (
|
||||
// hero,
|
||||
// HeroModel.A.attributes,
|
||||
// lookup (current_id)
|
||||
// ))
|
||||
|
||||
// const max_value =
|
||||
// getAttributeMaximum (current_id)
|
||||
// (mrace)
|
||||
// (HeroModel.A.attributeAdjustmentSelected (hero))
|
||||
// (startEl)
|
||||
// (currentEl)
|
||||
// (mphase)
|
||||
// (attributeValueLimit)
|
||||
|
||||
// const min_value =
|
||||
// getAttributeMinimum (wiki)
|
||||
// (hero)
|
||||
// (added)
|
||||
// (mblessed_primary_attr)
|
||||
// (mhighest_magical_primary_attr)
|
||||
// (hero_entry)
|
||||
|
||||
// return consF (AttributeWithRequirements ({
|
||||
// max: max_value,
|
||||
// min: min_value,
|
||||
// stateEntry: hero_entry,
|
||||
// wikiEntry: wiki_entry,
|
||||
// }))
|
||||
// })
|
||||
// (List.empty)
|
||||
// (StaticData.A.attributes (wiki)))
|
||||
// )
|
||||
|
||||
export const getCarryingCapacity = createSelector(
|
||||
selectDynamicAttributes,
|
||||
(attributes): number => (attributes[8]?.value ?? 8) * 2
|
||||
)
|
||||
|
||||
// export const getAdjustmentValue = createMaybeSelector (
|
||||
// getRace,
|
||||
// fmap (pipe (Race.A.attributeAdjustmentsSelection, fst))
|
||||
// )
|
||||
|
||||
// export const getCurrentAttributeAdjustment = createMaybeSelector (
|
||||
// getCurrentAttributeAdjustmentId,
|
||||
// getAttributesForView,
|
||||
// uncurryN (blackbirdF (liftM2 ((id: string) => find (pipe (AWRA.wikiEntry, AA.id, equals (id)))))
|
||||
// (join as join<Record<AttributeWithRequirements>>))
|
||||
// )
|
||||
|
||||
// export const getAvailableAdjustmentIds = createMaybeSelector (
|
||||
// getRace,
|
||||
// getAdjustmentValue,
|
||||
// getAttributesForView,
|
||||
// getCurrentAttributeAdjustment,
|
||||
// (mrace, madjustmentValue, mattrsCalculated, mcurr_attr) =>
|
||||
// fmapF (mrace)
|
||||
// (pipe (
|
||||
// Race.A.attributeAdjustmentsSelection,
|
||||
// snd,
|
||||
// adjustmentIds => {
|
||||
// if (isJust (mcurr_attr)) {
|
||||
// const curr_attr = fromJust (mcurr_attr)
|
||||
|
||||
// const curr_attr_val = pipe_ (curr_attr, AWRA.stateEntry, AtDA.value)
|
||||
|
||||
// if (or (pipe_ (curr_attr, AWRA.max, liftM2 (blackbirdF (subtractBy)
|
||||
// (lt (curr_attr_val)))
|
||||
// (madjustmentValue)))) {
|
||||
// const curr_attr_id = pipe_ (curr_attr, AWRA.stateEntry, AtDA.id)
|
||||
|
||||
// return List (curr_attr_id)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return filter ((id: string) => {
|
||||
// const mattr = bind (mattrsCalculated)
|
||||
// (find (pipe (AWRA.wikiEntry, AA.id, equals (id))))
|
||||
|
||||
// if (isJust (mattr)) {
|
||||
// const attr = fromJust (mattr)
|
||||
|
||||
// const mmax = AWRA.max (attr)
|
||||
|
||||
// const mcurr_attr_id = fmapF (mcurr_attr)
|
||||
// (pipe (AWRA.stateEntry, AtDA.id))
|
||||
|
||||
// if (isNothing (mmax) || Maybe.elem (id) (mcurr_attr_id)) {
|
||||
// return true
|
||||
// }
|
||||
|
||||
// if (isJust (madjustmentValue)) {
|
||||
// const attr_val = pipe_ (attr, AWRA.stateEntry, AtDA.value)
|
||||
|
||||
// return maybe (true)
|
||||
// (pipe (
|
||||
// add (fromJust (madjustmentValue)),
|
||||
// gte (attr_val)
|
||||
// ))
|
||||
// (mmax)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return false
|
||||
// })
|
||||
// (adjustmentIds)
|
||||
// }
|
||||
// ))
|
||||
// )
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { selectIsCharacterCreationFinished } from "../slices/characterSlice.ts"
|
||||
|
||||
export const selectIsInCharacterCreation = createSelector(
|
||||
selectIsCharacterCreationFinished,
|
||||
(isCharacterCreationFinished): boolean => !isCharacterCreationFinished
|
||||
)
|
||||
@@ -0,0 +1,489 @@
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { DerivedCharacteristic } from "optolith-database-schema/types/DerivedCharacteristic"
|
||||
import { firstLevel } from "../../shared/domain/activatableEntry.ts"
|
||||
import { modifierByIsActive, modifierByIsActives, modifierByLevel } from "../../shared/domain/activatableModifiers.ts"
|
||||
import { AdvantageIdentifier, AttributeIdentifier, CombatSpecialAbilityIdentifier, DerivedCharacteristicIdentifier as DCId, DisadvantageIdentifier, OptionalRuleIdentifier } from "../../shared/domain/identifier.ts"
|
||||
import { Rated } from "../../shared/domain/ratedEntry.ts"
|
||||
import { filterNonNullable } from "../../shared/utils/array.ts"
|
||||
import { createPropertySelector } from "../../shared/utils/redux.ts"
|
||||
import { attributeValue } from "../slices/attributesSlice.ts"
|
||||
import { selectActiveOptionalRules, selectAdvantages, selectAttributes, selectCombatSpecialAbilities, selectDisadvantages, selectLifePointsPermanentlyLost, selectPurchasedLifePoints } from "../slices/characterSlice.ts"
|
||||
import { selectDerivedCharacteristics as selectStaticDerivedCharacteristics } from "../slices/databaseSlice.ts"
|
||||
import { selectCurrentRace } from "./raceSelectors.ts"
|
||||
|
||||
// const SDA = StaticData.A
|
||||
// const ACA = AttributeCombined.A
|
||||
// const ADA = AttributeDependent.A
|
||||
// const DCA = DerivedCharacteristic.A
|
||||
// const MTA = MagicalTradition.A
|
||||
|
||||
// const divideByXAndRound = (x: number) => (a: number) => Math.round(a / x)
|
||||
// const divideBy2AndRound = divideByXAndRound(2)
|
||||
// const divideBy6AndRound = divideByXAndRound(6)
|
||||
|
||||
// const getFirstLevel =
|
||||
// pipe(
|
||||
// bindF(pipe(ActivatableDependent.A.active, listToMaybe)),
|
||||
// bindF(ActiveObject.A.tier)
|
||||
// )
|
||||
|
||||
export type DisplayedDerivedCharacteristic<T extends DCId = DCId> = {
|
||||
id: T
|
||||
base: number
|
||||
value: number
|
||||
modifier: number
|
||||
purchaseMaximum?: number
|
||||
purchased?: number
|
||||
permanentlyLost?: number
|
||||
permanentlyLostBoughtBack?: number
|
||||
static: DerivedCharacteristic
|
||||
}
|
||||
|
||||
export const isDisplayedEnergy = (
|
||||
dc: DisplayedDerivedCharacteristic
|
||||
): dc is DisplayedDerivedCharacteristic<DCId.LifePoints | DCId.ArcaneEnergy | DCId.KarmaPoints> =>
|
||||
dc.id === DCId.LifePoints || dc.id === DCId.ArcaneEnergy || dc.id === DCId.KarmaPoints
|
||||
|
||||
export const selectLifePoints = createSelector(
|
||||
selectCurrentRace,
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Constitution),
|
||||
createPropertySelector(selectAdvantages, AdvantageIdentifier.IncreasedLifePoints),
|
||||
createPropertySelector(selectDisadvantages, DisadvantageIdentifier.DecreasedLifePoints),
|
||||
selectLifePointsPermanentlyLost,
|
||||
selectPurchasedLifePoints,
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.LifePoints),
|
||||
(
|
||||
race,
|
||||
constitution,
|
||||
incrementor,
|
||||
decrementor,
|
||||
permanentlyLost,
|
||||
purchased,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.LifePoints> | undefined => {
|
||||
if (race === undefined || staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const base = race.base_values.life_points + attributeValue(constitution) * 2
|
||||
const modifier = modifierByLevel(incrementor, decrementor)
|
||||
const value = base + modifier + purchased - permanentlyLost
|
||||
|
||||
return {
|
||||
id: DCId.LifePoints,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
purchaseMaximum: attributeValue(constitution),
|
||||
purchased,
|
||||
permanentlyLost,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// export const getAE = createMaybeSelector(
|
||||
// getMagicalTraditionStaticEntries,
|
||||
// getHighestPrimaryMagicalAttributeValue,
|
||||
// getPermanentArcaneEnergyPoints,
|
||||
// mapGetToMaybeSlice(getAdvantages)(AdvantageId.IncreasedAstralPower),
|
||||
// mapGetToMaybeSlice(getDisadvantages)(DisadvantageId.DecreasedArcanePower),
|
||||
// getAddedArcaneEnergyPoints,
|
||||
// mapGetToSlice(getSpecialAbilities)(SpecialAbilityId.GrosseMeditation),
|
||||
// getWiki,
|
||||
// (trads, mprimary_value, paep, minc, mdec, added, mgreat_meditation, staticData) =>
|
||||
// pipe_(
|
||||
// staticData,
|
||||
// SDA.derivedCharacteristics,
|
||||
// lookup("AE"),
|
||||
// fmap((dc: Record<DerivedCharacteristic>) => {
|
||||
// const mlast_trad = listToMaybe(trads)
|
||||
|
||||
// const mredeemed = fmap(PermanentEnergyLossAndBoughtBack.A.redeemed)(paep)
|
||||
|
||||
// const mlost = fmap(PermanentEnergyLossAndBoughtBack.A.lost)(paep)
|
||||
|
||||
// const great_meditation_level = getFirstLevel(mgreat_meditation)
|
||||
|
||||
// const great_meditation_mod = maybe(0)(multiply(6))(great_meditation_level)
|
||||
|
||||
// const mod = great_meditation_mod
|
||||
// + modifyByLevelM(liftM2(subtract)(mredeemed)(mlost))
|
||||
// (minc)
|
||||
// (mdec)
|
||||
|
||||
// /**
|
||||
// * `Maybe (base, maxAdd)`
|
||||
// */
|
||||
// const mbaseAndAdd =
|
||||
// fmapF(mlast_trad)
|
||||
// (last_trad => fromMaybe(Tuple(20, 0, 0))
|
||||
// (fmapF(mprimary_value)
|
||||
// (primary_value => {
|
||||
// const ae_mod = pipe_(
|
||||
// last_trad,
|
||||
// sel1,
|
||||
// MTA.aeMod,
|
||||
// Maybe.product
|
||||
// )
|
||||
|
||||
// const maxAdd = Math.round(primary_value * ae_mod)
|
||||
|
||||
// return Tuple(maxAdd + 20, maxAdd, ae_mod)
|
||||
// })))
|
||||
|
||||
// const value = fmapF(mbaseAndAdd)
|
||||
// (pipe(sel1, base => base + mod + Maybe.sum(added)))
|
||||
|
||||
// const calc = pipe_(
|
||||
// mbaseAndAdd,
|
||||
// bindF(pipe(
|
||||
// sel3,
|
||||
// ae_mod =>
|
||||
// ae_mod === 1
|
||||
// ? Nothing
|
||||
// : ae_mod === 0.5
|
||||
// ? DCA.calcHalfPrimary(dc)
|
||||
// : DCA.calcNoPrimary(dc)
|
||||
// ))
|
||||
// )
|
||||
|
||||
// return DerivedCharacteristicValues<DCId.AE>({
|
||||
// add: Just(Maybe.sum(added)),
|
||||
// base: fmapF(mbaseAndAdd)(sel1),
|
||||
// calc,
|
||||
// currentAdd: Just(Maybe.sum(added)),
|
||||
// id: DCId.AE,
|
||||
// maxAdd: Just(Maybe.maybe(0)(sel2)(mbaseAndAdd)),
|
||||
// mod: Just(mod),
|
||||
// permanentLost: Just(Maybe.sum(mlost)),
|
||||
// permanentRedeemed: Just(Maybe.sum(mredeemed)),
|
||||
// value,
|
||||
// })
|
||||
// })
|
||||
// )
|
||||
// )
|
||||
|
||||
// export const getKP = createMaybeSelector(
|
||||
// getPrimaryBlessedAttribute,
|
||||
// getPermanentKarmaPoints,
|
||||
// mapGetToMaybeSlice(getAdvantages)(AdvantageId.IncreasedKarmaPoints),
|
||||
// mapGetToMaybeSlice(getDisadvantages)(DisadvantageId.DecreasedKarmaPoints),
|
||||
// getAddedKarmaPoints,
|
||||
// mapGetToSlice(getSpecialAbilities)(SpecialAbilityId.HoheWeihe),
|
||||
// (mprimary, pkp, minc, mdec, added, mhigh_consecration) => {
|
||||
// const mredeemed = fmap(PermanentEnergyLossAndBoughtBack.A.redeemed)(pkp)
|
||||
|
||||
// const mlost = fmap(PermanentEnergyLossAndBoughtBack.A.lost)(pkp)
|
||||
|
||||
// const highConsecrationLevel = getFirstLevel(mhigh_consecration)
|
||||
|
||||
// const highConsecrationMod = maybe(0)(multiply(6))(highConsecrationLevel)
|
||||
|
||||
// const mod = highConsecrationMod
|
||||
// + modifyByLevelM(liftM2(subtract)(mredeemed)(mlost))
|
||||
// (minc)
|
||||
// (mdec)
|
||||
|
||||
// const mbase = fmapF(mprimary)(pipe(ACA.stateEntry, ADA.value, add(20)))
|
||||
|
||||
// const value = fmapF(mbase)(base => base + mod + Maybe.sum(added))
|
||||
|
||||
// return DerivedCharacteristicValues<DCId.KP>({
|
||||
// add: Just(Maybe.sum(added)),
|
||||
// base: mbase,
|
||||
// currentAdd: Just(Maybe.sum(added)),
|
||||
// id: DCId.KP,
|
||||
// maxAdd: Just(Maybe.sum(fmapF(mprimary)(pipe(ACA.stateEntry, ADA.value)))),
|
||||
// mod: Just(mod),
|
||||
// permanentLost: Just(Maybe.sum(mlost)),
|
||||
// permanentRedeemed: Just(Maybe.sum(mredeemed)),
|
||||
// value,
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
|
||||
const divideAttributeSumByRound = (attributes: (Rated | undefined)[], divisor: number) =>
|
||||
Math.round(attributes.reduce((acc, attr) => acc + attributeValue(attr), 0) / divisor)
|
||||
|
||||
export const selectSpirit = createSelector(
|
||||
selectCurrentRace,
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Courage),
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Sagacity),
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Intuition),
|
||||
createPropertySelector(selectAdvantages, AdvantageIdentifier.IncreasedSpirit),
|
||||
createPropertySelector(selectDisadvantages, DisadvantageIdentifier.DecreasedSpirit),
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.Spirit),
|
||||
(
|
||||
race,
|
||||
cou,
|
||||
sgc,
|
||||
int,
|
||||
incrementor,
|
||||
decrementor,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.Spirit> | undefined => {
|
||||
if (race === undefined || staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const base = race.base_values.spirit + divideAttributeSumByRound([ cou, sgc, int ], 6)
|
||||
const modifier = modifierByIsActive(incrementor, decrementor)
|
||||
const value = base + modifier
|
||||
|
||||
return {
|
||||
id: DCId.Spirit,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectToughness = createSelector(
|
||||
selectCurrentRace,
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Constitution),
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Strength),
|
||||
createPropertySelector(selectAdvantages, AdvantageIdentifier.IncreasedToughness),
|
||||
createPropertySelector(selectDisadvantages, DisadvantageIdentifier.DecreasedToughness),
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.Toughness),
|
||||
(
|
||||
race,
|
||||
con,
|
||||
str,
|
||||
incrementor,
|
||||
decrementor,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.Toughness> | undefined => {
|
||||
if (race === undefined || staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const base = race.base_values.toughness + divideAttributeSumByRound([ con, con, str ], 6)
|
||||
const modifier = modifierByIsActive(incrementor, decrementor)
|
||||
const value = base + modifier
|
||||
|
||||
return {
|
||||
id: DCId.Toughness,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectDodge = createSelector(
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Agility),
|
||||
createPropertySelector(selectActiveOptionalRules, OptionalRuleIdentifier.HigherDefenseStats),
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.Dodge),
|
||||
(
|
||||
agi,
|
||||
higherDefenseStats,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.Dodge> | undefined => {
|
||||
if (staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const base = divideAttributeSumByRound([ agi ], 2)
|
||||
const modifier = (higherDefenseStats?.options?.[0] ?? 2) / 2
|
||||
const value = base + modifier
|
||||
|
||||
return {
|
||||
id: DCId.Dodge,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectInitiative = createSelector(
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Courage),
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Agility),
|
||||
// eslint-disable-next-line max-len
|
||||
createPropertySelector(selectCombatSpecialAbilities, CombatSpecialAbilityIdentifier.CombatReflexes),
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.Initiative),
|
||||
(
|
||||
cou,
|
||||
agi,
|
||||
combatReflexes,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.Initiative> | undefined => {
|
||||
if (staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const base = divideAttributeSumByRound([ cou, agi ], 2)
|
||||
const modifier = firstLevel(combatReflexes)
|
||||
const value = base + modifier
|
||||
|
||||
return {
|
||||
id: DCId.Initiative,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectMovement = createSelector(
|
||||
selectCurrentRace,
|
||||
createPropertySelector(selectAdvantages, AdvantageIdentifier.Nimble),
|
||||
createPropertySelector(selectAdvantages, AdvantageIdentifier.LeichterGang),
|
||||
createPropertySelector(selectDisadvantages, DisadvantageIdentifier.Maimed),
|
||||
createPropertySelector(selectDisadvantages, DisadvantageIdentifier.Slow),
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.Movement),
|
||||
(
|
||||
race,
|
||||
mimble,
|
||||
leichterGang,
|
||||
maimed,
|
||||
slow,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.Movement> | undefined => {
|
||||
if (race === undefined || staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const oneLegged = 3
|
||||
const isOneLeggedActive = maimed?.instances.some(
|
||||
instance =>
|
||||
instance.options?.[0]?.type === "Predefined"
|
||||
&& instance.options?.[0]?.id.type === "Generic"
|
||||
&& instance.options?.[0]?.id.value === oneLegged
|
||||
) ?? false
|
||||
|
||||
const base = isOneLeggedActive
|
||||
? Math.round(race.base_values.movement / 2)
|
||||
: race.base_values.movement
|
||||
|
||||
const modifier = modifierByIsActives([ mimble, leichterGang ], [ slow ])
|
||||
const value = base + modifier
|
||||
|
||||
return {
|
||||
id: DCId.Movement,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectWoundThreshold = createSelector(
|
||||
createPropertySelector(selectAttributes, AttributeIdentifier.Constitution),
|
||||
createPropertySelector(selectAdvantages, AdvantageIdentifier.Unyielding),
|
||||
createPropertySelector(selectDisadvantages, DisadvantageIdentifier.BrittleBones),
|
||||
createPropertySelector(selectStaticDerivedCharacteristics, DCId.WoundThreshold),
|
||||
(
|
||||
con,
|
||||
incrementor,
|
||||
decrementor,
|
||||
staticEntry,
|
||||
): DisplayedDerivedCharacteristic<typeof DCId.WoundThreshold> | undefined => {
|
||||
if (staticEntry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
else {
|
||||
const base = divideAttributeSumByRound([ con ], 2)
|
||||
const modifier = modifierByIsActive(incrementor, decrementor)
|
||||
const value = base + modifier
|
||||
|
||||
return {
|
||||
id: DCId.WoundThreshold,
|
||||
base,
|
||||
value,
|
||||
modifier,
|
||||
static: staticEntry,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// export type DCPair = Pair<Record<DerivedCharacteristic>, Record<DerivedCharacteristicValues>>
|
||||
|
||||
// export const getDerivedCharacteristicsMap = createMaybeSelector(
|
||||
// getLP,
|
||||
// getAE,
|
||||
// getKP,
|
||||
// getSPI,
|
||||
// getTOU,
|
||||
// getDO,
|
||||
// getINI,
|
||||
// getMOV,
|
||||
// getWT,
|
||||
// getRuleBooksEnabled,
|
||||
// getWiki,
|
||||
// (LP, AE, KP, SPI, TOU, DO, INI, MOV, WT, rule_books_enabled, staticData) => {
|
||||
// const isWoundThresholdEnabled = uncurry3(isBookEnabled)
|
||||
// (sourceBooksPairToTuple(rule_books_enabled))
|
||||
// ("US25003")
|
||||
|
||||
// return pipe_(
|
||||
// staticData,
|
||||
// SDA.derivedCharacteristics,
|
||||
// mapMaybe((x): Maybe<DCPair> => {
|
||||
// switch (DCA.id(x)) {
|
||||
// case "LP":
|
||||
// return Just(Pair(x, LP))
|
||||
// case "AE":
|
||||
// return fmapF(AE)(Pair(x))
|
||||
// case "KP":
|
||||
// return Just(Pair(x, KP))
|
||||
// case "SPI":
|
||||
// return Just(Pair(x, SPI))
|
||||
// case "TOU":
|
||||
// return Just(Pair(x, TOU))
|
||||
// case "DO":
|
||||
// return Just(Pair(x, DO))
|
||||
// case "INI":
|
||||
// return Just(Pair(x, INI))
|
||||
// case "MOV":
|
||||
// return Just(Pair(x, MOV))
|
||||
// case "WT":
|
||||
// return isWoundThresholdEnabled ? Just(Pair(x, WT)) : Nothing
|
||||
// default:
|
||||
// return Nothing
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
|
||||
export const selectDerivedCharacteristics = createSelector(
|
||||
selectLifePoints,
|
||||
selectSpirit,
|
||||
selectToughness,
|
||||
selectDodge,
|
||||
selectInitiative,
|
||||
selectMovement,
|
||||
selectWoundThreshold,
|
||||
(
|
||||
lifePoints,
|
||||
spirit,
|
||||
toughness,
|
||||
dodge,
|
||||
initiative,
|
||||
movement,
|
||||
woundThreshold,
|
||||
): DisplayedDerivedCharacteristic[] => filterNonNullable([
|
||||
lifePoints,
|
||||
spirit,
|
||||
toughness,
|
||||
dodge,
|
||||
initiative,
|
||||
movement,
|
||||
woundThreshold,
|
||||
])
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { ExperienceLevel } from "optolith-database-schema/types/ExperienceLevel"
|
||||
import { selectExperienceLevelStartId, selectTotalAdventurePoints } from "../slices/characterSlice.ts"
|
||||
import { selectExperienceLevels } from "../slices/databaseSlice.ts"
|
||||
|
||||
export const selectStartExperienceLevel = createSelector(
|
||||
selectExperienceLevels,
|
||||
selectExperienceLevelStartId,
|
||||
(experienceLevels, experienceLevelStartId): ExperienceLevel | undefined =>
|
||||
experienceLevelStartId === undefined ? undefined : experienceLevels[experienceLevelStartId]
|
||||
)
|
||||
|
||||
export const selectCurrentExperienceLevel = createSelector(
|
||||
selectExperienceLevels,
|
||||
selectTotalAdventurePoints,
|
||||
(experienceLevels, totalAdventurePoints): ExperienceLevel | undefined =>
|
||||
totalAdventurePoints === undefined
|
||||
? undefined
|
||||
: Object.values(experienceLevels)
|
||||
.sort((a, b) => a.adventure_points - b.adventure_points)
|
||||
.reduce(
|
||||
(acc, experienceLevel) =>
|
||||
experienceLevel.adventure_points <= totalAdventurePoints
|
||||
? experienceLevel
|
||||
: acc,
|
||||
experienceLevels[0]
|
||||
)
|
||||
)
|
||||
|
||||
export const selectMaximumTotalAttributePoints = createSelector(
|
||||
selectStartExperienceLevel,
|
||||
(experienceLevel): number => experienceLevel?.max_attribute_total ?? 0
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { Race } from "optolith-database-schema/types/Race"
|
||||
import { selectRaceId } from "../slices/characterSlice.ts"
|
||||
import { selectRaces } from "../slices/databaseSlice.ts"
|
||||
|
||||
export const selectCurrentRace = createSelector(
|
||||
selectRaces,
|
||||
selectRaceId,
|
||||
(races, id): Race | undefined => id === undefined ? undefined : races[id]
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ActionReducerMapBuilder, Draft, createAction } from "@reduxjs/toolkit"
|
||||
import { ImprovementCost } from "../../shared/domain/adventurePoints/improvementCost.ts"
|
||||
import { cachedAdventurePoints } from "../../shared/domain/adventurePoints/ratedEntry.ts"
|
||||
import { Rated } from "../../shared/domain/ratedEntry.ts"
|
||||
import { CharacterState } from "./characterSlice.ts"
|
||||
|
||||
const minValue = 8
|
||||
const getImprovementCost = (_id: number) => ImprovementCost.E
|
||||
|
||||
const updateCachedAdventurePoints = (entry: Draft<Rated>) => {
|
||||
entry.cachedAdventurePoints = cachedAdventurePoints(
|
||||
entry.value,
|
||||
minValue,
|
||||
entry.boundAdventurePoints,
|
||||
getImprovementCost(entry.id),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new entry with an initial value if active. The initial adventure
|
||||
* points cache is calculated from the initial value.
|
||||
*/
|
||||
export const createDynamicAttribute = (id: number, value: number = minValue): Rated => ({
|
||||
id,
|
||||
value: Math.max(minValue, value),
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
dependencies: [],
|
||||
boundAdventurePoints: [],
|
||||
})
|
||||
|
||||
/**
|
||||
* Takes an entry that may not exist (because its instance has not been used
|
||||
* yet) and returns its value.
|
||||
*/
|
||||
export const attributeValue = (entry: Rated | undefined): number => entry?.value ?? minValue
|
||||
|
||||
export const incrementAttribute = createAction<number>("attributes/incrementAttribute")
|
||||
export const decrementAttribute = createAction<number>("attributes/decrementAttribute")
|
||||
|
||||
export const attributesReducer = (builder: ActionReducerMapBuilder<CharacterState>) =>
|
||||
builder
|
||||
.addCase(incrementAttribute, (state, action) => {
|
||||
const entry = state.attributes[action.payload] ??= createDynamicAttribute(action.payload)
|
||||
entry.value++
|
||||
updateCachedAdventurePoints(entry)
|
||||
})
|
||||
.addCase(decrementAttribute, (state, action) => {
|
||||
const entry = state.attributes[action.payload]
|
||||
if (entry !== undefined && entry.value > minValue) {
|
||||
entry.value--
|
||||
updateCachedAdventurePoints(entry)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,809 @@
|
||||
/* eslint-disable max-len */
|
||||
import { createReducer } from "@reduxjs/toolkit"
|
||||
import { ActivatableRated, ActivatableRatedWithEnhancements, Rated } from "../../shared/domain/ratedEntry.ts"
|
||||
import { RootState } from "../store.ts"
|
||||
import { attributesReducer } from "./attributesSlice.ts"
|
||||
import { derivedCharacteristicsReducer } from "./derivedCharacteristicsSlice.ts"
|
||||
|
||||
export type CharacterState = {
|
||||
/**
|
||||
* A valid semantic version (https://semver.org), representing the Optolith version this character was created with.
|
||||
*/
|
||||
version: string | undefined
|
||||
|
||||
/**
|
||||
* The character's name.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Date of character creation, in ISO8601 format.
|
||||
*/
|
||||
dateCreated: string
|
||||
|
||||
/**
|
||||
* Date of character last modified, in ISO8601 format.
|
||||
*/
|
||||
dateLastModified: string
|
||||
|
||||
/**
|
||||
* Total adventure points.
|
||||
*/
|
||||
totalAdventurePoints: number
|
||||
|
||||
/**
|
||||
* The start experience level identifier.
|
||||
*/
|
||||
experienceLevelStartId: number
|
||||
|
||||
/**
|
||||
* Describes whether the character creation is finished.
|
||||
*/
|
||||
isCharacterCreationFinished: boolean
|
||||
|
||||
/**
|
||||
* An object storing the identifiers of the race and its optional race variant.
|
||||
*/
|
||||
race: {
|
||||
/**
|
||||
* The base race identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* The race variant identifier.
|
||||
*/
|
||||
variantId?: number
|
||||
|
||||
/**
|
||||
* The identifier of the attribute adjustment that has been selected from the race.
|
||||
*/
|
||||
selectedAttributeAdjustmentId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* An object storing the identifier of the culture and if the cultural package has been applied.
|
||||
*/
|
||||
culture: {
|
||||
/**
|
||||
* The culture identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* Describes whether the cultural package has been applied when creating the character.
|
||||
*/
|
||||
isCulturalPackageApplied: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* An object storing the identifiers of the profession, its instance and its optional profession variant.
|
||||
*/
|
||||
profession: {
|
||||
/**
|
||||
* The base profession identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* The profession instance identifier. Profession instances are versions of the same profession but slightly different values, such as in extension rule books existing professions might get an additional style special ability.
|
||||
*/
|
||||
instanceId: number
|
||||
|
||||
/**
|
||||
* The profession variant identifier.
|
||||
*/
|
||||
variantId?: number
|
||||
|
||||
/**
|
||||
* A custom name for the profession, if provided.
|
||||
*/
|
||||
customName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* An object storing the identifiers of the profession, its instance and its optional race variant.
|
||||
*/
|
||||
curriculum?: {
|
||||
/**
|
||||
* The educational institution's curriculum identifier.
|
||||
*/
|
||||
educationalInstitutionId: number
|
||||
|
||||
/**
|
||||
* The lesson package identifier.
|
||||
*/
|
||||
lessonPackageId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The rules settings for the character.
|
||||
*/
|
||||
rules: {
|
||||
/**
|
||||
* Whether the character makes use of all publications except for publications with adult content, which have to be specified explicitly using `include_publications`.
|
||||
*/
|
||||
includeAllPublications: boolean
|
||||
|
||||
/**
|
||||
* Explicitly used publications. If `include_all_publications` is set to `true`, only affects publications that are not covered by `include_all_publications`.
|
||||
*/
|
||||
includePublications: number[]
|
||||
|
||||
/**
|
||||
* Active focus rules.
|
||||
*/
|
||||
activeFocusRules: {
|
||||
[id: number]: ActiveFocusRule
|
||||
}
|
||||
|
||||
/**
|
||||
* Active optional rules.
|
||||
*/
|
||||
activeOptionalRules: {
|
||||
[id: number]: ActiveOptionalRule
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Personal data such as hair color and place of birth.
|
||||
*/
|
||||
personalData: {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
sex: Sex
|
||||
|
||||
/**
|
||||
* The family names and/or family members.
|
||||
*/
|
||||
family?: string
|
||||
|
||||
/**
|
||||
* The place where the character was born.
|
||||
*/
|
||||
placeOfBirth?: string
|
||||
|
||||
/**
|
||||
* The date when the character was born.
|
||||
*/
|
||||
dateOfBirth?: string
|
||||
|
||||
/**
|
||||
* The age of the character.
|
||||
*/
|
||||
age?: string
|
||||
|
||||
/**
|
||||
* The hair color of the character.
|
||||
*/
|
||||
hairColor?: Color
|
||||
|
||||
/**
|
||||
* The eye color of the character.
|
||||
*/
|
||||
eyeColor?: Color
|
||||
|
||||
/**
|
||||
* The size of the character.
|
||||
*/
|
||||
size?: string
|
||||
|
||||
/**
|
||||
* The weight of the character.
|
||||
*/
|
||||
weight?: string
|
||||
|
||||
/**
|
||||
* The character's title(s).
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* The social status identifier.
|
||||
*/
|
||||
socialStatusId?: number
|
||||
|
||||
/**
|
||||
* The character's characteristics.
|
||||
*/
|
||||
characteristics?: string
|
||||
|
||||
/**
|
||||
* Other information about the character.
|
||||
*/
|
||||
otherInfo?: string
|
||||
}
|
||||
|
||||
advantages: ActivatableMap
|
||||
disadvantages: ActivatableMap
|
||||
|
||||
/**
|
||||
* Lists of active special abilities, by group.
|
||||
*/
|
||||
specialAbilities: {
|
||||
advancedCombatSpecialAbilities: ActivatableMap
|
||||
advancedKarmaSpecialAbilities: ActivatableMap
|
||||
advancedMagicalSpecialAbilities: ActivatableMap
|
||||
advancedSkillSpecialAbilities: ActivatableMap
|
||||
ancestorGlyphs: ActivatableMap
|
||||
arcaneOrbEnchantments: ActivatableMap
|
||||
attireEnchantments: ActivatableMap
|
||||
blessedTraditions: ActivatableMap
|
||||
bowlEnchantments: ActivatableMap
|
||||
brawlingSpecialAbilities: ActivatableMap
|
||||
cauldronEnchantments: ActivatableMap
|
||||
ceremonialItemSpecialAbilities: ActivatableMap
|
||||
chronicleEnchantments: ActivatableMap
|
||||
combatSpecialAbilities: ActivatableMap
|
||||
combatStyleSpecialAbilities: ActivatableMap
|
||||
commandSpecialAbilities: ActivatableMap
|
||||
daggerRituals: ActivatableMap
|
||||
familiarSpecialAbilities: ActivatableMap
|
||||
fatePointSexSpecialAbilities: ActivatableMap
|
||||
fatePointSpecialAbilities: ActivatableMap
|
||||
foolsHatEnchantments: ActivatableMap
|
||||
generalSpecialAbilities: ActivatableMap
|
||||
instrumentEnchantments: ActivatableMap
|
||||
karmaSpecialAbilities: ActivatableMap
|
||||
krallenkettenzauber: ActivatableMap
|
||||
liturgicalStyleSpecialAbilities: ActivatableMap
|
||||
lycantropicGifts: ActivatableMap
|
||||
magicalRunes: ActivatableMap
|
||||
magicalSpecialAbilities: ActivatableMap
|
||||
magicalTraditions: ActivatableMap
|
||||
magicStyleSpecialAbilities: ActivatableMap
|
||||
orbEnchantments: ActivatableMap
|
||||
pactGifts: ActivatableMap
|
||||
protectiveWardingCircleSpecialAbilities: ActivatableMap
|
||||
ringEnchantments: ActivatableMap
|
||||
sermons: ActivatableMap
|
||||
sexSpecialAbilities: ActivatableMap
|
||||
sickleRituals: ActivatableMap
|
||||
sikaryanDrainSpecialAbilities: ActivatableMap
|
||||
skillStyleSpecialAbilities: ActivatableMap
|
||||
spellSwordEnchantments: ActivatableMap
|
||||
staffEnchantments: ActivatableMap
|
||||
toyEnchantments: ActivatableMap
|
||||
trinkhornzauber: ActivatableMap
|
||||
vampiricGifts: ActivatableMap
|
||||
visions: ActivatableMap
|
||||
wandEnchantments: ActivatableMap
|
||||
weaponEnchantments: ActivatableMap
|
||||
}
|
||||
|
||||
attributes: RatedMap
|
||||
|
||||
derivedCharacteristics: {
|
||||
lifePoints: Energy
|
||||
arcaneEnergy: EnergyWithBuyBack
|
||||
karmaPoints: EnergyWithBuyBack
|
||||
}
|
||||
|
||||
skills: RatedMap
|
||||
|
||||
combatTechniques: {
|
||||
close: RatedMap
|
||||
ranged: RatedMap
|
||||
}
|
||||
|
||||
cantrips: TinyActivatableSet
|
||||
spells: ActivatableRatedWithEnhancementsMap
|
||||
rituals: ActivatableRatedWithEnhancementsMap
|
||||
|
||||
magicalActions: {
|
||||
curses: ActivatableRatedMap
|
||||
elvenMagicalSongs: ActivatableRatedMap
|
||||
dominationRituals: ActivatableRatedMap
|
||||
magicalDances: ActivatableRatedMap
|
||||
magicalMelodies: ActivatableRatedMap
|
||||
jesterTricks: ActivatableRatedMap
|
||||
animistPowers: ActivatableRatedMap
|
||||
geodeRituals: ActivatableRatedMap
|
||||
zibiljaRituals: ActivatableRatedMap
|
||||
}
|
||||
|
||||
blessings: TinyActivatableSet
|
||||
liturgicalChants: ActivatableRatedWithEnhancementsMap
|
||||
ceremonies: ActivatableRatedWithEnhancementsMap
|
||||
|
||||
// items: {}
|
||||
// hitZoneArmors: {}
|
||||
purse: Purse
|
||||
// creatures: {}
|
||||
// pact: {}
|
||||
}
|
||||
|
||||
export type ActiveFocusRule = {
|
||||
/**
|
||||
* The focus rule identifier.
|
||||
*/
|
||||
id: number
|
||||
}
|
||||
|
||||
export type ActiveOptionalRule = {
|
||||
/**
|
||||
* The optional rule identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* An array of one or more options. The exact meaning of each option varies based on the optional rule.
|
||||
*/
|
||||
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 Color =
|
||||
| PredefinedColor
|
||||
| CustomColor
|
||||
|
||||
/**
|
||||
* A predefined color.
|
||||
*/
|
||||
export type PredefinedColor = {
|
||||
type: "Predefined"
|
||||
|
||||
/**
|
||||
* The color identifier.
|
||||
*/
|
||||
id: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom color.
|
||||
*/
|
||||
export type CustomColor = {
|
||||
type: "Custom"
|
||||
|
||||
/**
|
||||
* The custom color name.
|
||||
*/
|
||||
name: string
|
||||
}
|
||||
|
||||
export type Energy = {
|
||||
/**
|
||||
* The number of points purchased.
|
||||
*/
|
||||
purchased: number
|
||||
|
||||
/**
|
||||
* The number of points permanently lost.
|
||||
*/
|
||||
permanentlyLost: number
|
||||
}
|
||||
|
||||
export type EnergyWithBuyBack = {
|
||||
/**
|
||||
* The number of points purchased.
|
||||
*/
|
||||
purchased: number
|
||||
|
||||
/**
|
||||
* The number of points permanently lost.
|
||||
*/
|
||||
permanentlyLost: number
|
||||
|
||||
/**
|
||||
* The number of permanently lost points that have been bought back.
|
||||
*/
|
||||
permanentlyLostBoughtBack: number
|
||||
}
|
||||
|
||||
export type RatedMap = {
|
||||
[id: number]: Rated
|
||||
}
|
||||
|
||||
export type ActivatableRatedMap = {
|
||||
[id: number]: ActivatableRated
|
||||
}
|
||||
|
||||
export type ActivatableRatedWithEnhancementsMap = {
|
||||
[id: number]: ActivatableRatedWithEnhancements
|
||||
}
|
||||
|
||||
export type TinyActivatableSet = number[]
|
||||
|
||||
/**
|
||||
* The money the character owns.
|
||||
* @title Purse
|
||||
*/
|
||||
export type Purse = {
|
||||
/**
|
||||
* The number of kreutzers the character owns.
|
||||
* @minimum 0
|
||||
* @integer
|
||||
*/
|
||||
kreutzers: number
|
||||
|
||||
/**
|
||||
* The number of halers the character owns.
|
||||
* @minimum 0
|
||||
* @integer
|
||||
*/
|
||||
halers: number
|
||||
|
||||
/**
|
||||
* The number of silverthalers the character owns.
|
||||
* @minimum 0
|
||||
* @integer
|
||||
*/
|
||||
silverthalers: number
|
||||
|
||||
/**
|
||||
* The number of ducats the character owns.
|
||||
* @minimum 0
|
||||
* @integer
|
||||
*/
|
||||
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?: (
|
||||
| {
|
||||
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
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: "Custom"
|
||||
|
||||
/**
|
||||
* A user-entered text.
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
)[]
|
||||
|
||||
/**
|
||||
* 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 ActivatableMap = {
|
||||
[id: number]: Activatable
|
||||
}
|
||||
|
||||
const initialState = (): CharacterState => ({
|
||||
version: undefined,
|
||||
name: "",
|
||||
dateCreated: new Date().toISOString(),
|
||||
dateLastModified: new Date().toISOString(),
|
||||
totalAdventurePoints: 1100,
|
||||
experienceLevelStartId: 3,
|
||||
isCharacterCreationFinished: false,
|
||||
race: {
|
||||
id: 1,
|
||||
selectedAttributeAdjustmentId: 1,
|
||||
},
|
||||
culture: {
|
||||
id: 1,
|
||||
isCulturalPackageApplied: false,
|
||||
},
|
||||
profession: {
|
||||
id: 1,
|
||||
instanceId: 1,
|
||||
},
|
||||
rules: {
|
||||
includeAllPublications: false,
|
||||
includePublications: [],
|
||||
activeFocusRules: [],
|
||||
activeOptionalRules: [],
|
||||
},
|
||||
personalData: {
|
||||
sex: { type: "Male" },
|
||||
},
|
||||
advantages: {},
|
||||
disadvantages: {},
|
||||
specialAbilities: {
|
||||
advancedCombatSpecialAbilities: {},
|
||||
advancedKarmaSpecialAbilities: {},
|
||||
advancedMagicalSpecialAbilities: {},
|
||||
advancedSkillSpecialAbilities: {},
|
||||
ancestorGlyphs: {},
|
||||
arcaneOrbEnchantments: {},
|
||||
attireEnchantments: {},
|
||||
blessedTraditions: {},
|
||||
bowlEnchantments: {},
|
||||
brawlingSpecialAbilities: {},
|
||||
cauldronEnchantments: {},
|
||||
ceremonialItemSpecialAbilities: {},
|
||||
chronicleEnchantments: {},
|
||||
combatSpecialAbilities: {},
|
||||
combatStyleSpecialAbilities: {},
|
||||
commandSpecialAbilities: {},
|
||||
daggerRituals: {},
|
||||
familiarSpecialAbilities: {},
|
||||
fatePointSexSpecialAbilities: {},
|
||||
fatePointSpecialAbilities: {},
|
||||
foolsHatEnchantments: {},
|
||||
generalSpecialAbilities: {},
|
||||
instrumentEnchantments: {},
|
||||
karmaSpecialAbilities: {},
|
||||
krallenkettenzauber: {},
|
||||
liturgicalStyleSpecialAbilities: {},
|
||||
lycantropicGifts: {},
|
||||
magicalRunes: {},
|
||||
magicalSpecialAbilities: {},
|
||||
magicalTraditions: {},
|
||||
magicStyleSpecialAbilities: {},
|
||||
orbEnchantments: {},
|
||||
pactGifts: {},
|
||||
protectiveWardingCircleSpecialAbilities: {},
|
||||
ringEnchantments: {},
|
||||
sermons: {},
|
||||
sexSpecialAbilities: {},
|
||||
sickleRituals: {},
|
||||
sikaryanDrainSpecialAbilities: {},
|
||||
skillStyleSpecialAbilities: {},
|
||||
spellSwordEnchantments: {},
|
||||
staffEnchantments: {},
|
||||
toyEnchantments: {},
|
||||
trinkhornzauber: {},
|
||||
vampiricGifts: {},
|
||||
visions: {},
|
||||
wandEnchantments: {},
|
||||
weaponEnchantments: {},
|
||||
},
|
||||
attributes: {},
|
||||
derivedCharacteristics: {
|
||||
lifePoints: {
|
||||
purchased: 0,
|
||||
permanentlyLost: 0,
|
||||
},
|
||||
arcaneEnergy: {
|
||||
purchased: 0,
|
||||
permanentlyLost: 0,
|
||||
permanentlyLostBoughtBack: 0,
|
||||
},
|
||||
karmaPoints: {
|
||||
purchased: 0,
|
||||
permanentlyLost: 0,
|
||||
permanentlyLostBoughtBack: 0,
|
||||
},
|
||||
},
|
||||
skills: {},
|
||||
combatTechniques: {
|
||||
close: {},
|
||||
ranged: {},
|
||||
},
|
||||
cantrips: [],
|
||||
spells: {},
|
||||
rituals: {},
|
||||
magicalActions: {
|
||||
curses: {},
|
||||
elvenMagicalSongs: {},
|
||||
dominationRituals: {},
|
||||
magicalDances: {},
|
||||
magicalMelodies: {},
|
||||
jesterTricks: {},
|
||||
animistPowers: {},
|
||||
geodeRituals: {},
|
||||
zibiljaRituals: {},
|
||||
},
|
||||
blessings: [],
|
||||
liturgicalChants: {},
|
||||
ceremonies: {},
|
||||
// items: {}
|
||||
// hitZoneArmors: {}
|
||||
purse: {
|
||||
kreutzers: 0,
|
||||
halers: 0,
|
||||
silverthalers: 0,
|
||||
ducats: 0,
|
||||
},
|
||||
// creatures: {}
|
||||
// pact: {}
|
||||
})
|
||||
|
||||
export const selectCurrentCharacter = (state: RootState) =>
|
||||
state.characters.selectedId === undefined
|
||||
? undefined
|
||||
: state.characters.characters[state.characters.selectedId]
|
||||
|
||||
export const selectTotalAdventurePoints = (state: RootState) => selectCurrentCharacter(state)?.totalAdventurePoints
|
||||
export const selectExperienceLevelStartId = (state: RootState) => selectCurrentCharacter(state)?.experienceLevelStartId
|
||||
export const selectIsCharacterCreationFinished = (state: RootState) => selectCurrentCharacter(state)?.isCharacterCreationFinished ?? false
|
||||
export const selectRaceId = (state: RootState) => selectCurrentCharacter(state)?.race.id
|
||||
export const selectRaceVariantId = (state: RootState) => selectCurrentCharacter(state)?.race.variantId
|
||||
export const selectAttributeAdjustmentId = (state: RootState) => selectCurrentCharacter(state)?.race.selectedAttributeAdjustmentId
|
||||
export const selectActiveFocusRules = (state: RootState) => selectCurrentCharacter(state)?.rules.activeFocusRules ?? {}
|
||||
export const selectActiveOptionalRules = (state: RootState) => selectCurrentCharacter(state)?.rules.activeOptionalRules ?? {}
|
||||
export const selectAdvantages = (state: RootState) => selectCurrentCharacter(state)?.advantages ?? {}
|
||||
export const selectDisadvantages = (state: RootState) => selectCurrentCharacter(state)?.disadvantages ?? {}
|
||||
export const selectAdvancedCombatSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.advancedCombatSpecialAbilities ?? {}
|
||||
export const selectAdvancedKarmaSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.advancedKarmaSpecialAbilities ?? {}
|
||||
export const selectAdvancedMagicalSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.advancedMagicalSpecialAbilities ?? {}
|
||||
export const selectAdvancedSkillSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.advancedSkillSpecialAbilities ?? {}
|
||||
export const selectAncestorGlyphs = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.ancestorGlyphs ?? {}
|
||||
export const selectArcaneOrbEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.arcaneOrbEnchantments ?? {}
|
||||
export const selectAttireEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.attireEnchantments ?? {}
|
||||
export const selectBlessedTraditions = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.blessedTraditions ?? {}
|
||||
export const selectBowlEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.bowlEnchantments ?? {}
|
||||
export const selectBrawlingSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.brawlingSpecialAbilities ?? {}
|
||||
export const selectCauldronEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.cauldronEnchantments ?? {}
|
||||
export const selectCeremonialItemSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.ceremonialItemSpecialAbilities ?? {}
|
||||
export const selectChronicleEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.chronicleEnchantments ?? {}
|
||||
export const selectCombatSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.combatSpecialAbilities ?? {}
|
||||
export const selectCombatStyleSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.combatStyleSpecialAbilities ?? {}
|
||||
export const selectCommandSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.commandSpecialAbilities ?? {}
|
||||
export const selectDaggerRituals = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.daggerRituals ?? {}
|
||||
export const selectFamiliarSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.familiarSpecialAbilities ?? {}
|
||||
export const selectFatePointSexSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.fatePointSexSpecialAbilities ?? {}
|
||||
export const selectFatePointSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.fatePointSpecialAbilities ?? {}
|
||||
export const selectFoolsHatEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.foolsHatEnchantments ?? {}
|
||||
export const selectGeneralSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.generalSpecialAbilities ?? {}
|
||||
export const selectInstrumentEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.instrumentEnchantments ?? {}
|
||||
export const selectKarmaSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.karmaSpecialAbilities ?? {}
|
||||
export const selectKrallenkettenzauber = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.krallenkettenzauber ?? {}
|
||||
export const selectLiturgicalStyleSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.liturgicalStyleSpecialAbilities ?? {}
|
||||
export const selectLycantropicGifts = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.lycantropicGifts ?? {}
|
||||
export const selectMagicalRunes = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.magicalRunes ?? {}
|
||||
export const selectMagicalSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.magicalSpecialAbilities ?? {}
|
||||
export const selectMagicalTraditions = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.magicalTraditions ?? {}
|
||||
export const selectMagicStyleSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.magicStyleSpecialAbilities ?? {}
|
||||
export const selectOrbEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.orbEnchantments ?? {}
|
||||
export const selectPactGifts = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.pactGifts ?? {}
|
||||
export const selectProtectiveWardingCircleSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.protectiveWardingCircleSpecialAbilities ?? {}
|
||||
export const selectRingEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.ringEnchantments ?? {}
|
||||
export const selectSermons = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.sermons ?? {}
|
||||
export const selectSexSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.sexSpecialAbilities ?? {}
|
||||
export const selectSickleRituals = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.sickleRituals ?? {}
|
||||
export const selectSikaryanDrainSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.sikaryanDrainSpecialAbilities ?? {}
|
||||
export const selectSkillStyleSpecialAbilities = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.skillStyleSpecialAbilities ?? {}
|
||||
export const selectSpellSwordEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.spellSwordEnchantments ?? {}
|
||||
export const selectStaffEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.staffEnchantments ?? {}
|
||||
export const selectToyEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.toyEnchantments ?? {}
|
||||
export const selectTrinkhornzauber = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.trinkhornzauber ?? {}
|
||||
export const selectVampiricGifts = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.vampiricGifts ?? {}
|
||||
export const selectVisions = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.visions ?? {}
|
||||
export const selectWandEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.wandEnchantments ?? {}
|
||||
export const selectWeaponEnchantments = (state: RootState) => selectCurrentCharacter(state)?.specialAbilities.weaponEnchantments ?? {}
|
||||
export const selectAttributes = (state: RootState) => selectCurrentCharacter(state)?.attributes ?? {}
|
||||
export const selectDerivedCharacteristics = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics ?? {
|
||||
lifePoints: {
|
||||
purchased: 0,
|
||||
permanentlyLost: 0,
|
||||
},
|
||||
arcaneEnergy: {
|
||||
purchased: 0,
|
||||
permanentlyLost: 0,
|
||||
permanentlyLostBoughtBack: 0,
|
||||
},
|
||||
karmaPoints: {
|
||||
purchased: 0,
|
||||
permanentlyLost: 0,
|
||||
permanentlyLostBoughtBack: 0,
|
||||
},
|
||||
}
|
||||
export const selectPurchasedLifePoints = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.lifePoints.purchased ?? 0
|
||||
export const selectLifePointsPermanentlyLost = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.lifePoints.permanentlyLost ?? 0
|
||||
export const selectPurchasedArcaneEnergy = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.arcaneEnergy.purchased ?? 0
|
||||
export const selectArcaneEnergyPermanentlyLost = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.arcaneEnergy.permanentlyLost ?? 0
|
||||
export const selectArcaneEnergyPermanentlyLostBoughtBack = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack ?? 0
|
||||
export const selectPurchasedKarmaPoints = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.karmaPoints.purchased ?? 0
|
||||
export const selectKarmaPointsPermanentlyLost = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.karmaPoints.permanentlyLost ?? 0
|
||||
export const selectKarmaPointsPermanentlyLostBoughtBack = (state: RootState) => selectCurrentCharacter(state)?.derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack ?? 0
|
||||
|
||||
export const characterReducer = createReducer(initialState, builder => {
|
||||
attributesReducer(builder)
|
||||
derivedCharacteristicsReducer(builder)
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
import { RootState } from "../store.ts"
|
||||
import { CharacterState, characterReducer } from "./characterSlice.ts"
|
||||
|
||||
type CharactersState = {
|
||||
selectedId?: string
|
||||
characters: Record<string, CharacterState>
|
||||
}
|
||||
|
||||
const initialState: CharactersState = {
|
||||
selectedId: "550e8400-e29b-11d4-a716-446655440000",
|
||||
characters: {},
|
||||
}
|
||||
|
||||
const charactersSlice = createSlice({
|
||||
name: "characters",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addDefaultCase((state, action) => {
|
||||
if (state.selectedId !== undefined) {
|
||||
state.characters[state.selectedId] =
|
||||
characterReducer(state.characters[state.selectedId], action)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// export const {} = localeSlice.actions
|
||||
|
||||
export const selectSelectedCharacterId = (state: RootState) => state.characters.selectedId
|
||||
export const selectCharacters = (state: RootState) => state.characters.characters
|
||||
|
||||
export const charactersReducer = charactersSlice.reducer
|
||||
@@ -2,6 +2,7 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { TypeId, TypeMap } from "optolith-database-schema/config/types"
|
||||
import { Database as RawDatabase } from "../../database/index.ts"
|
||||
import { RootState } from "../store.ts"
|
||||
|
||||
export type DatabaseState = {
|
||||
[K in keyof TypeMap]: Record<TypeId<K>, TypeMap[K]>
|
||||
@@ -327,4 +328,159 @@ const databaseSlice = createSlice({
|
||||
|
||||
export const { initDatabase } = databaseSlice.actions
|
||||
|
||||
|
||||
export const selectAdvancedCombatSpecialAbilities = (state: RootState) => state.database.advancedCombatSpecialAbilities
|
||||
export const selectAdvancedKarmaSpecialAbilities = (state: RootState) => state.database.advancedKarmaSpecialAbilities
|
||||
export const selectAdvancedMagicalSpecialAbilities = (state: RootState) => state.database.advancedMagicalSpecialAbilities
|
||||
export const selectAdvancedSkillSpecialAbilities = (state: RootState) => state.database.advancedSkillSpecialAbilities
|
||||
export const selectAdvantages = (state: RootState) => state.database.advantages
|
||||
export const selectAlchemicae = (state: RootState) => state.database.alchemicae
|
||||
export const selectAmmunition = (state: RootState) => state.database.ammunition
|
||||
export const selectAncestorGlyphs = (state: RootState) => state.database.ancestorGlyphs
|
||||
export const selectAnimalCare = (state: RootState) => state.database.animalCare
|
||||
export const selectAnimalDiseases = (state: RootState) => state.database.animalDiseases
|
||||
export const selectAnimals = (state: RootState) => state.database.animals
|
||||
export const selectAnimalShapePaths = (state: RootState) => state.database.animalShapePaths
|
||||
export const selectAnimalShapes = (state: RootState) => state.database.animalShapes
|
||||
export const selectAnimalShapeSizes = (state: RootState) => state.database.animalShapeSizes
|
||||
export const selectAnimalTypes = (state: RootState) => state.database.animalTypes
|
||||
export const selectAnimistPowers = (state: RootState) => state.database.animistPowers
|
||||
export const selectAnimistPowerTribes = (state: RootState) => state.database.animistPowerTribes
|
||||
export const selectArcaneBardTraditions = (state: RootState) => state.database.arcaneBardTraditions
|
||||
export const selectArcaneDancerTraditions = (state: RootState) => state.database.arcaneDancerTraditions
|
||||
export const selectArcaneOrbEnchantments = (state: RootState) => state.database.arcaneOrbEnchantments
|
||||
export const selectArmors = (state: RootState) => state.database.armors
|
||||
export const selectArmorTypes = (state: RootState) => state.database.armorTypes
|
||||
export const selectAspects = (state: RootState) => state.database.aspects
|
||||
export const selectAttireEnchantments = (state: RootState) => state.database.attireEnchantments
|
||||
export const selectAttributes = (state: RootState) => state.database.attributes
|
||||
export const selectBandagesAndRemedies = (state: RootState) => state.database.bandagesAndRemedies
|
||||
export const selectBlessedTraditions = (state: RootState) => state.database.blessedTraditions
|
||||
export const selectBlessings = (state: RootState) => state.database.blessings
|
||||
export const selectBooks = (state: RootState) => state.database.books
|
||||
export const selectBowlEnchantments = (state: RootState) => state.database.bowlEnchantments
|
||||
export const selectBrawlingSpecialAbilities = (state: RootState) => state.database.brawlingSpecialAbilities
|
||||
export const selectBrews = (state: RootState) => state.database.brews
|
||||
export const selectCantrips = (state: RootState) => state.database.cantrips
|
||||
export const selectCauldronEnchantments = (state: RootState) => state.database.cauldronEnchantments
|
||||
export const selectCeremonialItems = (state: RootState) => state.database.ceremonialItems
|
||||
export const selectCeremonialItemSpecialAbilities = (state: RootState) => state.database.ceremonialItemSpecialAbilities
|
||||
export const selectCeremonies = (state: RootState) => state.database.ceremonies
|
||||
export const selectChronicleEnchantments = (state: RootState) => state.database.chronicleEnchantments
|
||||
export const selectCloseCombatTechniques = (state: RootState) => state.database.closeCombatTechniques
|
||||
export const selectClothes = (state: RootState) => state.database.clothes
|
||||
export const selectCombatSpecialAbilities = (state: RootState) => state.database.combatSpecialAbilities
|
||||
export const selectCombatStyleSpecialAbilities = (state: RootState) => state.database.combatStyleSpecialAbilities
|
||||
export const selectCommandSpecialAbilities = (state: RootState) => state.database.commandSpecialAbilities
|
||||
export const selectConditions = (state: RootState) => state.database.conditions
|
||||
export const selectContainers = (state: RootState) => state.database.containers
|
||||
export const selectContinents = (state: RootState) => state.database.continents
|
||||
export const selectCoreRules = (state: RootState) => state.database.coreRules
|
||||
export const selectCultures = (state: RootState) => state.database.cultures
|
||||
export const selectCurses = (state: RootState) => state.database.curses
|
||||
export const selectDaggerRituals = (state: RootState) => state.database.daggerRituals
|
||||
export const selectDerivedCharacteristics = (state: RootState) => state.database.derivedCharacteristics
|
||||
export const selectDisadvantages = (state: RootState) => state.database.disadvantages
|
||||
export const selectDiseases = (state: RootState) => state.database.diseases
|
||||
export const selectDominationRituals = (state: RootState) => state.database.dominationRituals
|
||||
export const selectElements = (state: RootState) => state.database.elements
|
||||
export const selectElixirs = (state: RootState) => state.database.elixirs
|
||||
export const selectElvenMagicalSongs = (state: RootState) => state.database.elvenMagicalSongs
|
||||
export const selectEquipmentOfBlessedOnes = (state: RootState) => state.database.equipmentOfBlessedOnes
|
||||
export const selectEquipmentPackages = (state: RootState) => state.database.equipmentPackages
|
||||
export const selectExperienceLevels = (state: RootState) => state.database.experienceLevels
|
||||
export const selectEyeColors = (state: RootState) => state.database.eyeColors
|
||||
export const selectFamiliarSpecialAbilities = (state: RootState) => state.database.familiarSpecialAbilities
|
||||
export const selectFamiliarsTricks = (state: RootState) => state.database.familiarsTricks
|
||||
export const selectFatePointSexSpecialAbilities = (state: RootState) => state.database.fatePointSexSpecialAbilities
|
||||
export const selectFatePointSpecialAbilities = (state: RootState) => state.database.fatePointSpecialAbilities
|
||||
export const selectFocusRules = (state: RootState) => state.database.focusRules
|
||||
export const selectFocusRuleSubjects = (state: RootState) => state.database.focusRuleSubjects
|
||||
export const selectFoolsHatEnchantments = (state: RootState) => state.database.foolsHatEnchantments
|
||||
export const selectGemsAndPreciousStones = (state: RootState) => state.database.gemsAndPreciousStones
|
||||
export const selectGeneralSpecialAbilities = (state: RootState) => state.database.generalSpecialAbilities
|
||||
export const selectGeodeRituals = (state: RootState) => state.database.geodeRituals
|
||||
export const selectHairColors = (state: RootState) => state.database.hairColors
|
||||
export const selectIlluminationLightSources = (state: RootState) => state.database.illuminationLightSources
|
||||
export const selectIlluminationRefillsAndSupplies = (state: RootState) => state.database.illuminationRefillsAndSupplies
|
||||
export const selectInstrumentEnchantments = (state: RootState) => state.database.instrumentEnchantments
|
||||
export const selectJesterTricks = (state: RootState) => state.database.jesterTricks
|
||||
export const selectJewelry = (state: RootState) => state.database.jewelry
|
||||
export const selectKarmaSpecialAbilities = (state: RootState) => state.database.karmaSpecialAbilities
|
||||
export const selectKirchenpraegungen = (state: RootState) => state.database.kirchenpraegungen
|
||||
export const selectKrallenkettenzauber = (state: RootState) => state.database.krallenkettenzauber
|
||||
export const selectLanguages = (state: RootState) => state.database.languages
|
||||
export const selectLessonsCurricula = (state: RootState) => state.database.lessonsCurricula
|
||||
export const selectLessonsGuidelines = (state: RootState) => state.database.lessonsGuidelines
|
||||
export const selectLiebesspielzeug = (state: RootState) => state.database.liebesspielzeug
|
||||
export const selectLiturgicalChants = (state: RootState) => state.database.liturgicalChants
|
||||
export const selectLiturgicalStyleSpecialAbilities = (state: RootState) => state.database.liturgicalStyleSpecialAbilities
|
||||
export const selectLocales = (state: RootState) => state.database.locales
|
||||
export const selectLuxuryGoods = (state: RootState) => state.database.luxuryGoods
|
||||
export const selectLycantropicGifts = (state: RootState) => state.database.lycantropicGifts
|
||||
export const selectMagicalArtifacts = (state: RootState) => state.database.magicalArtifacts
|
||||
export const selectMagicalDances = (state: RootState) => state.database.magicalDances
|
||||
export const selectMagicalMelodies = (state: RootState) => state.database.magicalMelodies
|
||||
export const selectMagicalRunes = (state: RootState) => state.database.magicalRunes
|
||||
export const selectMagicalSigns = (state: RootState) => state.database.magicalSigns
|
||||
export const selectMagicalSpecialAbilities = (state: RootState) => state.database.magicalSpecialAbilities
|
||||
export const selectMagicalTraditions = (state: RootState) => state.database.magicalTraditions
|
||||
export const selectMagicStyleSpecialAbilities = (state: RootState) => state.database.magicStyleSpecialAbilities
|
||||
export const selectMetaConditions = (state: RootState) => state.database.metaConditions
|
||||
export const selectMusicalInstruments = (state: RootState) => state.database.musicalInstruments
|
||||
export const selectOptionalRules = (state: RootState) => state.database.optionalRules
|
||||
export const selectOrbEnchantments = (state: RootState) => state.database.orbEnchantments
|
||||
export const selectOrienteeringAids = (state: RootState) => state.database.orienteeringAids
|
||||
export const selectPactCategories = (state: RootState) => state.database.pactCategories
|
||||
export const selectPactGifts = (state: RootState) => state.database.pactGifts
|
||||
export const selectPatronCategories = (state: RootState) => state.database.patronCategories
|
||||
export const selectPatrons = (state: RootState) => state.database.patrons
|
||||
export const selectPersonalityTraits = (state: RootState) => state.database.personalityTraits
|
||||
export const selectPoisons = (state: RootState) => state.database.poisons
|
||||
export const selectProfessions = (state: RootState) => state.database.professions
|
||||
export const selectProperties = (state: RootState) => state.database.properties
|
||||
export const selectProtectiveWardingCircleSpecialAbilities = (state: RootState) => state.database.protectiveWardingCircleSpecialAbilities
|
||||
export const selectPublications = (state: RootState) => state.database.publications
|
||||
export const selectRaces = (state: RootState) => state.database.races
|
||||
export const selectRangedCombatTechniques = (state: RootState) => state.database.rangedCombatTechniques
|
||||
export const selectReaches = (state: RootState) => state.database.reaches
|
||||
export const selectRegions = (state: RootState) => state.database.regions
|
||||
export const selectRingEnchantments = (state: RootState) => state.database.ringEnchantments
|
||||
export const selectRituals = (state: RootState) => state.database.rituals
|
||||
export const selectRopesAndChains = (state: RootState) => state.database.ropesAndChains
|
||||
export const selectScripts = (state: RootState) => state.database.scripts
|
||||
export const selectSermons = (state: RootState) => state.database.sermons
|
||||
export const selectServices = (state: RootState) => state.database.services
|
||||
export const selectSexPractices = (state: RootState) => state.database.sexPractices
|
||||
export const selectSexSpecialAbilities = (state: RootState) => state.database.sexSpecialAbilities
|
||||
export const selectSickleRituals = (state: RootState) => state.database.sickleRituals
|
||||
export const selectSikaryanDrainSpecialAbilities = (state: RootState) => state.database.sikaryanDrainSpecialAbilities
|
||||
export const selectSkillGroups = (state: RootState) => state.database.skillGroups
|
||||
export const selectSkillModificationLevels = (state: RootState) => state.database.skillModificationLevels
|
||||
export const selectSkills = (state: RootState) => state.database.skills
|
||||
export const selectSkillStyleSpecialAbilities = (state: RootState) => state.database.skillStyleSpecialAbilities
|
||||
export const selectSocialStatuses = (state: RootState) => state.database.socialStatuses
|
||||
export const selectSpells = (state: RootState) => state.database.spells
|
||||
export const selectSpellSwordEnchantments = (state: RootState) => state.database.spellSwordEnchantments
|
||||
export const selectStaffEnchantments = (state: RootState) => state.database.staffEnchantments
|
||||
export const selectStates = (state: RootState) => state.database.states
|
||||
export const selectStationary = (state: RootState) => state.database.stationary
|
||||
export const selectTalismans = (state: RootState) => state.database.talismans
|
||||
export const selectTargetCategories = (state: RootState) => state.database.targetCategories
|
||||
export const selectThievesTools = (state: RootState) => state.database.thievesTools
|
||||
export const selectToolsOfTheTrade = (state: RootState) => state.database.toolsOfTheTrade
|
||||
export const selectToyEnchantments = (state: RootState) => state.database.toyEnchantments
|
||||
export const selectTradeSecrets = (state: RootState) => state.database.tradeSecrets
|
||||
export const selectTravelGearAndTools = (state: RootState) => state.database.travelGearAndTools
|
||||
export const selectTrinkhornzauber = (state: RootState) => state.database.trinkhornzauber
|
||||
export const selectUi = (state: RootState) => state.database.ui
|
||||
export const selectVampiricGifts = (state: RootState) => state.database.vampiricGifts
|
||||
export const selectVehicles = (state: RootState) => state.database.vehicles
|
||||
export const selectVisions = (state: RootState) => state.database.visions
|
||||
export const selectWandEnchantments = (state: RootState) => state.database.wandEnchantments
|
||||
export const selectWeaponAccessories = (state: RootState) => state.database.weaponAccessories
|
||||
export const selectWeaponEnchantments = (state: RootState) => state.database.weaponEnchantments
|
||||
export const selectWeapons = (state: RootState) => state.database.weapons
|
||||
export const selectZibiljaRituals = (state: RootState) => state.database.zibiljaRituals
|
||||
|
||||
export const databaseReducer = databaseSlice.reducer
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/* eslint-disable max-len */
|
||||
import { ActionReducerMapBuilder, createAction } from "@reduxjs/toolkit"
|
||||
import { CharacterState } from "./characterSlice.ts"
|
||||
|
||||
export const incrementLifePoints = createAction("derivedCharacteristics/incrementLifePoints")
|
||||
export const decrementLifePoints = createAction("derivedCharacteristics/decrementLifePoints")
|
||||
export const incrementLifePointsPermanentlyLost = createAction("derivedCharacteristics/incrementLifePointsPermanentlyLost")
|
||||
export const decrementLifePointsPermanentlyLost = createAction("derivedCharacteristics/decrementLifePointsPermanentlyLost")
|
||||
export const addLifePointsPermanentlyLost = createAction<number>("derivedCharacteristics/addLifePointsPermanentlyLost")
|
||||
export const incrementArcaneEnergy = createAction("derivedCharacteristics/incrementArcaneEnergy")
|
||||
export const decrementArcaneEnergy = createAction("derivedCharacteristics/decrementArcaneEnergy")
|
||||
export const incrementArcaneEnergyPermanentlyLost = createAction("derivedCharacteristics/incrementArcaneEnergyPermanentlyLost")
|
||||
export const decrementArcaneEnergyPermanentlyLost = createAction("derivedCharacteristics/decrementArcaneEnergyPermanentlyLost")
|
||||
export const addArcaneEnergyPermanentlyLost = createAction<number>("derivedCharacteristics/addArcaneEnergyPermanentlyLost")
|
||||
export const incrementArcaneEnergyBoughtBack = createAction("derivedCharacteristics/incrementArcaneEnergyBoughtBack")
|
||||
export const decrementArcaneEnergyBoughtBack = createAction("derivedCharacteristics/decrementArcaneEnergyBoughtBack")
|
||||
export const incrementKarmaPoints = createAction("derivedCharacteristics/incrementKarmaPoints")
|
||||
export const decrementKarmaPoints = createAction("derivedCharacteristics/decrementKarmaPoints")
|
||||
export const incrementKarmaPointsPermanentlyLost = createAction("derivedCharacteristics/incrementKarmaPointsPermanentlyLost")
|
||||
export const decrementKarmaPointsPermanentlyLost = createAction("derivedCharacteristics/decrementKarmaPointsPermanentlyLost")
|
||||
export const addKarmaPointsPermanentlyLost = createAction<number>("derivedCharacteristics/addKarmaPointsPermanentlyLost")
|
||||
export const incrementKarmaPointsBoughtBack = createAction("derivedCharacteristics/incrementKarmaPointsBoughtBack")
|
||||
export const decrementKarmaPointsBoughtBack = createAction("derivedCharacteristics/decrementKarmaPointsBoughtBack")
|
||||
|
||||
export const derivedCharacteristicsReducer = (builder: ActionReducerMapBuilder<CharacterState>) =>
|
||||
builder
|
||||
.addCase(incrementLifePoints, (state, _action) => {
|
||||
state.derivedCharacteristics.lifePoints.purchased++
|
||||
})
|
||||
.addCase(decrementLifePoints, (state, _action) => {
|
||||
if (state.derivedCharacteristics.lifePoints.purchased > 0) {
|
||||
state.derivedCharacteristics.lifePoints.purchased--
|
||||
}
|
||||
})
|
||||
.addCase(incrementLifePointsPermanentlyLost, (state, _action) => {
|
||||
state.derivedCharacteristics.lifePoints.permanentlyLost++
|
||||
})
|
||||
.addCase(decrementLifePointsPermanentlyLost, (state, _action) => {
|
||||
if (state.derivedCharacteristics.lifePoints.permanentlyLost > 0) {
|
||||
state.derivedCharacteristics.lifePoints.permanentlyLost--
|
||||
}
|
||||
})
|
||||
.addCase(addLifePointsPermanentlyLost, (state, action) => {
|
||||
state.derivedCharacteristics.lifePoints.permanentlyLost += action.payload
|
||||
})
|
||||
.addCase(incrementArcaneEnergy, (state, _action) => {
|
||||
state.derivedCharacteristics.arcaneEnergy.purchased++
|
||||
})
|
||||
.addCase(decrementArcaneEnergy, (state, _action) => {
|
||||
if (state.derivedCharacteristics.arcaneEnergy.purchased > 0) {
|
||||
state.derivedCharacteristics.arcaneEnergy.purchased--
|
||||
}
|
||||
})
|
||||
.addCase(incrementArcaneEnergyPermanentlyLost, (state, _action) => {
|
||||
state.derivedCharacteristics.arcaneEnergy.permanentlyLost++
|
||||
})
|
||||
.addCase(decrementArcaneEnergyPermanentlyLost, (state, _action) => {
|
||||
if (state.derivedCharacteristics.arcaneEnergy.permanentlyLost > 0) {
|
||||
state.derivedCharacteristics.arcaneEnergy.permanentlyLost--
|
||||
}
|
||||
})
|
||||
.addCase(addArcaneEnergyPermanentlyLost, (state, action) => {
|
||||
state.derivedCharacteristics.arcaneEnergy.permanentlyLost += action.payload
|
||||
})
|
||||
.addCase(incrementArcaneEnergyBoughtBack, (state, _action) => {
|
||||
state.derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack++
|
||||
})
|
||||
.addCase(decrementArcaneEnergyBoughtBack, (state, _action) => {
|
||||
if (state.derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack > 0) {
|
||||
state.derivedCharacteristics.arcaneEnergy.permanentlyLostBoughtBack--
|
||||
}
|
||||
})
|
||||
.addCase(incrementKarmaPoints, (state, _action) => {
|
||||
state.derivedCharacteristics.karmaPoints.purchased++
|
||||
})
|
||||
.addCase(decrementKarmaPoints, (state, _action) => {
|
||||
if (state.derivedCharacteristics.karmaPoints.purchased > 0) {
|
||||
state.derivedCharacteristics.karmaPoints.purchased--
|
||||
}
|
||||
})
|
||||
.addCase(incrementKarmaPointsPermanentlyLost, (state, _action) => {
|
||||
state.derivedCharacteristics.karmaPoints.permanentlyLost++
|
||||
})
|
||||
.addCase(decrementKarmaPointsPermanentlyLost, (state, _action) => {
|
||||
if (state.derivedCharacteristics.karmaPoints.permanentlyLost > 0) {
|
||||
state.derivedCharacteristics.karmaPoints.permanentlyLost--
|
||||
}
|
||||
})
|
||||
.addCase(addKarmaPointsPermanentlyLost, (state, action) => {
|
||||
state.derivedCharacteristics.karmaPoints.permanentlyLost += action.payload
|
||||
})
|
||||
.addCase(incrementKarmaPointsBoughtBack, (state, _action) => {
|
||||
state.derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack++
|
||||
})
|
||||
.addCase(decrementKarmaPointsBoughtBack, (state, _action) => {
|
||||
if (state.derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack > 0) {
|
||||
state.derivedCharacteristics.karmaPoints.permanentlyLostBoughtBack--
|
||||
}
|
||||
})
|
||||
@@ -34,7 +34,7 @@ type RouteState = {
|
||||
}
|
||||
|
||||
const initialRouteState: RouteState = {
|
||||
route: "characters",
|
||||
route: "attributes",
|
||||
}
|
||||
|
||||
const routeSlice = createSlice({
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
import { Theme } from "../../shared/schema/config.ts"
|
||||
import { RootState } from "../store.ts"
|
||||
|
||||
type SettingsState = {
|
||||
locale: string
|
||||
theme: Theme
|
||||
areAnimationsEnabled: boolean
|
||||
}
|
||||
|
||||
const initialSettingsState: SettingsState = {
|
||||
locale: "de-DE",
|
||||
theme: Theme.Dark,
|
||||
areAnimationsEnabled: true,
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: "locale",
|
||||
name: "settings",
|
||||
initialState: initialSettingsState,
|
||||
reducers: {},
|
||||
})
|
||||
@@ -18,5 +23,7 @@ const settingsSlice = createSlice({
|
||||
// export const {} = localeSlice.actions
|
||||
|
||||
export const selectLocale = (state: RootState) => state.settings.locale
|
||||
export const selectTheme = (state: RootState) => state.settings.theme
|
||||
export const selectAreAnimationsEnabled = (state: RootState) => state.settings.areAnimationsEnabled
|
||||
|
||||
export const settingsReducer = settingsSlice.reducer
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { charactersReducer } from "./slices/charactersSlice.ts"
|
||||
import { databaseReducer } from "./slices/databaseSlice.ts"
|
||||
import { routeReducer } from "./slices/routeSlice.ts"
|
||||
import { settingsReducer } from "./slices/settingsSlice.ts"
|
||||
@@ -6,6 +7,7 @@ import { settingsReducer } from "./slices/settingsSlice.ts"
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
database: databaseReducer,
|
||||
characters: charactersReducer,
|
||||
route: routeReducer,
|
||||
settings: settingsReducer,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Activatable } from "../../main_window/slices/characterSlice.ts"
|
||||
|
||||
/**
|
||||
* Get the level of the first instance of a given activatable entry, if it is
|
||||
* active. Defaults to `0`.
|
||||
*/
|
||||
export const firstLevel = (activatable: Activatable | undefined) =>
|
||||
activatable?.instances?.[0]?.level ?? 0
|
||||
|
||||
/**
|
||||
* Returns if a present activatable entry is active. Defaults to `false`.
|
||||
*/
|
||||
export const isActive = (activatable: Activatable | undefined) =>
|
||||
(activatable?.instances.length ?? 0) > 0
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Activatable } from "../../main_window/slices/characterSlice.ts"
|
||||
import { firstLevel, isActive } from "./activatableEntry.ts"
|
||||
|
||||
/**
|
||||
* There are pairs of entries that are mutually exclusive and modify a certain
|
||||
* value by their level, either in a positive or a negative way. This function
|
||||
* returns the value modifier that is caused by the given pair of entries.
|
||||
*/
|
||||
export const modifierByLevel = (
|
||||
incrementor: Activatable | undefined,
|
||||
decrementor: Activatable | undefined,
|
||||
): number =>
|
||||
firstLevel(incrementor) - firstLevel(decrementor)
|
||||
|
||||
/**
|
||||
* There are pairs of entries that are mutually exclusive and modify a certain
|
||||
* value by being purchased, either in a positive or a negative way. This
|
||||
* function returns the value modifier that is caused by the given pair of
|
||||
* entries.
|
||||
*/
|
||||
export const modifierByIsActive = (
|
||||
incrementor: Activatable | undefined,
|
||||
decrementor: Activatable | undefined,
|
||||
): number =>
|
||||
isActive(incrementor) ? 1 : isActive(decrementor) ? -1 : 0
|
||||
|
||||
const countActive = (activatables: (Activatable | undefined)[]) =>
|
||||
activatables.reduce((acc, entry) => acc + (isActive(entry) ? 1 : 0), 0)
|
||||
|
||||
/**
|
||||
* There are entries that modify a certain value by being purchased, either in a
|
||||
* positive or a negative way. This function returns the value modifier that is
|
||||
* caused by the given entries.
|
||||
*/
|
||||
export const modifierByIsActives = (
|
||||
incrementors: (Activatable | undefined)[],
|
||||
decrementors: (Activatable | undefined)[],
|
||||
): number =>
|
||||
countActive(incrementors) - countActive(decrementors)
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { rangeSafe, sum } from "../utils/array.ts"
|
||||
import { assertExhaustive } from "../utils/typeSafety.ts"
|
||||
import { rangeSafe, sum } from "../../utils/array.ts"
|
||||
import { assertExhaustive } from "../../utils/typeSafety.ts"
|
||||
|
||||
export enum ImprovementCost {
|
||||
A = "A",
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
import { ImprovementCost } from "./improvementCost.ts"
|
||||
import { RatedAdventurePointsCache, cachedAdventurePoints } from "./ratedEntry.ts"
|
||||
|
||||
describe("cachedAdventurePoints", () => {
|
||||
it("returns the calculated value if bound adventure points are only granted at at least the current rating", () => {
|
||||
assert.deepEqual<RatedAdventurePointsCache>(
|
||||
cachedAdventurePoints(
|
||||
9,
|
||||
8,
|
||||
[ { rating: 9, adventurePoints: 10 } ],
|
||||
ImprovementCost.E,
|
||||
),
|
||||
{
|
||||
general: 15,
|
||||
bound: 0,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("returns the split calculated value if bound adventure points have to be considered", () => {
|
||||
assert.deepEqual<RatedAdventurePointsCache>(
|
||||
cachedAdventurePoints(
|
||||
9,
|
||||
8,
|
||||
[ { rating: 8, adventurePoints: 10 } ],
|
||||
ImprovementCost.E,
|
||||
),
|
||||
{
|
||||
general: 5,
|
||||
bound: 10,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
import { range } from "../../utils/array.ts"
|
||||
import { ImprovementCost, adventurePointsForIncrement } from "./improvementCost.ts"
|
||||
|
||||
/**
|
||||
* Bound adventure points are granted by the GM and can only be spent on the
|
||||
* entry. They don’t effect the costs of the rating at the time of granting, so
|
||||
* the rating at which they have been granted is stored as well.
|
||||
*/
|
||||
export type BoundAdventurePoints = {
|
||||
/**
|
||||
* The rating at which they have been granted. If the adventure points have
|
||||
* been granted when the entry was not active yet, the rating is `undefined`.
|
||||
*/
|
||||
rating: number | undefined
|
||||
|
||||
/**
|
||||
* The granted adventure points.
|
||||
*/
|
||||
adventurePoints: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The accumulated used adventure points value of all value increases. It is
|
||||
* split by used bound and used general adventure points.
|
||||
*/
|
||||
export type RatedAdventurePointsCache = {
|
||||
/**
|
||||
* The used general adventure points.
|
||||
*/
|
||||
general: number
|
||||
|
||||
/**
|
||||
* The used bound adventure points.
|
||||
*/
|
||||
bound: number
|
||||
}
|
||||
|
||||
const groupBoundAdventurePointsByRating = (
|
||||
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>(),
|
||||
)
|
||||
|
||||
const accumulateCache = (
|
||||
startValue: number,
|
||||
endValue: number,
|
||||
initialApplicableBoundKey: number | "activation",
|
||||
boundByValue: ReadonlyMap<number | "activation", number>,
|
||||
ic: ImprovementCost,
|
||||
): RatedAdventurePointsCache => {
|
||||
const { usedGeneral, usedBound } = range(startValue, endValue).reduce(
|
||||
(acc, currentValue) => {
|
||||
const costForStep = adventurePointsForIncrement(ic, currentValue - 1)
|
||||
const usedBoundForStep = Math.min(costForStep, acc.remainingApplicableBound)
|
||||
const usedGeneralForStep = costForStep - usedBoundForStep
|
||||
|
||||
const newRemainingBound = acc.remainingApplicableBound
|
||||
- usedBoundForStep
|
||||
+ (boundByValue.get(currentValue) ?? 0)
|
||||
|
||||
return {
|
||||
usedGeneral: acc.usedGeneral + usedGeneralForStep,
|
||||
usedBound: acc.usedBound + usedBoundForStep,
|
||||
remainingApplicableBound: newRemainingBound,
|
||||
}
|
||||
},
|
||||
{
|
||||
usedGeneral: 0,
|
||||
usedBound: 0,
|
||||
remainingApplicableBound: (boundByValue.get(initialApplicableBoundKey) ?? 0),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
general: usedGeneral,
|
||||
bound: usedBound,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the accumulated used adventure points value for a rated entry. It
|
||||
* takes into account the minimum value if it’s always active, bound adventure
|
||||
* points and the improvement cost of the entry.
|
||||
*/
|
||||
export const cachedAdventurePoints = (
|
||||
value: number,
|
||||
minValue: number,
|
||||
boundAdventurePoints: BoundAdventurePoints[],
|
||||
ic: ImprovementCost,
|
||||
): RatedAdventurePointsCache => {
|
||||
if (minValue >= value) {
|
||||
return {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
}
|
||||
}
|
||||
else {
|
||||
const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints)
|
||||
return accumulateCache(minValue + 1, value, minValue, boundByValue, ic)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the accumulated used adventure points value for an activatable
|
||||
* rated entry. It takes into account the minimum value if it’s always active,
|
||||
* bound adventure points and the improvement cost of the entry.
|
||||
*/
|
||||
export const cachedAdventurePointsForActivatable = (
|
||||
value: number | undefined,
|
||||
boundAdventurePoints: BoundAdventurePoints[],
|
||||
ic: ImprovementCost,
|
||||
): RatedAdventurePointsCache => {
|
||||
if (value === undefined) {
|
||||
return {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
}
|
||||
}
|
||||
else {
|
||||
const boundByValue = groupBoundAdventurePointsByRating(boundAdventurePoints)
|
||||
return accumulateCache(0, value, "activation", boundByValue, ic)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ActivatableIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
|
||||
|
||||
export type EnhancementDependency =
|
||||
| {
|
||||
tag: "Internal"
|
||||
|
||||
/**
|
||||
* The depending enhancement.
|
||||
*/
|
||||
id: number
|
||||
}
|
||||
| {
|
||||
tag: "External"
|
||||
|
||||
/**
|
||||
* The depending activatable.
|
||||
*/
|
||||
id: ActivatableIdentifier
|
||||
}
|
||||
|
||||
export type Enhancement = {
|
||||
id: number
|
||||
dependencies: EnhancementDependency[]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// TODO: Update for new identifier mappings
|
||||
|
||||
export namespace OptionalRuleIdentifier {
|
||||
export const HigherDefenseStats = 17
|
||||
}
|
||||
|
||||
export namespace AttributeIdentifier {
|
||||
export const Courage = 1
|
||||
export const Sagacity = 2
|
||||
export const Intuition = 3
|
||||
export const Charisma = 4
|
||||
export const Dexterity = 5
|
||||
export const Agility = 6
|
||||
export const Constitution = 7
|
||||
export const Strength = 8
|
||||
}
|
||||
|
||||
export enum DerivedCharacteristicIdentifier {
|
||||
LifePoints = 1,
|
||||
ArcaneEnergy = 2,
|
||||
KarmaPoints = 3,
|
||||
Spirit = 4,
|
||||
Toughness = 5,
|
||||
Dodge = 6,
|
||||
Initiative = 7,
|
||||
Movement = 8,
|
||||
FatePoints = 9,
|
||||
WoundThreshold = 10,
|
||||
}
|
||||
|
||||
export type EnergyIdentifier =
|
||||
| DerivedCharacteristicIdentifier.LifePoints
|
||||
| DerivedCharacteristicIdentifier.ArcaneEnergy
|
||||
| DerivedCharacteristicIdentifier.KarmaPoints
|
||||
|
||||
export namespace AdvantageIdentifier {
|
||||
export const CustomAdvantage = 0
|
||||
export const Aptitude = 4 // Begabung
|
||||
export const Nimble = 9 // Flink
|
||||
export const Blessed = 12
|
||||
export const Luck = 14
|
||||
export const ExceptionalSkill = 16
|
||||
export const ExceptionalCombatTechnique = 17
|
||||
export const IncreasedAstralPower = 23
|
||||
export const IncreasedKarmaPoints = 24
|
||||
export const IncreasedLifePoints = 25
|
||||
export const IncreasedSpirit = 26
|
||||
export const IncreasedToughness = 27
|
||||
export const ImmunityToPoison = 28
|
||||
export const ImmunityToDisease = 29
|
||||
export const MagicalAttunement = 32
|
||||
export const Rich = 36
|
||||
export const SociallyAdaptable = 40
|
||||
export const InspireConfidence = 46
|
||||
export const WeaponAptitude = 47
|
||||
export const Spellcaster = 50
|
||||
export const Unyielding = 54 // Eisern
|
||||
export const LargeSpellSelection = 58
|
||||
export const HatredOf = 68
|
||||
export const Prediger = 77
|
||||
export const Visionaer = 78
|
||||
export const ZahlreichePredigten = 79
|
||||
export const ZahlreicheVisionen = 80
|
||||
export const LeichterGang = 92
|
||||
export const Einkommen = 99
|
||||
}
|
||||
|
||||
export namespace DisadvantageIdentifier {
|
||||
export const CustomDisadvantage = 0
|
||||
export const AfraidOf = 1
|
||||
export const Poor = 2
|
||||
export const Slow = 4
|
||||
export const NoFlyingBalm = 17
|
||||
export const NoFamiliar = 18
|
||||
export const MagicalRestriction = 24
|
||||
export const DecreasedArcanePower = 26
|
||||
export const DecreasedKarmaPoints = 27
|
||||
export const DecreasedLifePoints = 28
|
||||
export const DecreasedSpirit = 29
|
||||
export const DecreasedToughness = 30
|
||||
export const BadLuck = 31
|
||||
export const PersonalityFlaw = 33
|
||||
export const Principles = 34
|
||||
export const BadHabit = 36
|
||||
export const NegativeTrait = 37 // Schlechte Eigenschaft
|
||||
export const Stigma = 45
|
||||
export const Deaf = 47 // Taub
|
||||
export const Incompetent = 48
|
||||
export const Obligations = 50 // Verpflichtungen
|
||||
export const Maimed = 51 // Verstümmelt
|
||||
export const BrittleBones = 56 // Gläsern
|
||||
export const SmallSpellSelection = 59
|
||||
export const WenigePredigten = 72
|
||||
export const WenigeVisionen = 73
|
||||
}
|
||||
|
||||
export namespace CombatSpecialAbilityIdentifier {
|
||||
export const CombatReflexes = 12
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
import { ImprovementCost } from "./adventurePoints/improvementCost.ts"
|
||||
import { Rated, createRatedHelpers } from "./ratedEntry.ts"
|
||||
|
||||
describe("createRatedHelpers", () => {
|
||||
const helpers = createRatedHelpers({
|
||||
minValue: 8,
|
||||
getImprovementCost: _ => ImprovementCost.E,
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("returns initial values if no optional values are provided", () => {
|
||||
assert.deepEqual<Rated>(
|
||||
helpers.create(1),
|
||||
{
|
||||
id: 1,
|
||||
value: 8,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("calculates adventure points cache if a specific rating is provided", () => {
|
||||
assert.deepEqual<Rated>(
|
||||
helpers.create(1, 9),
|
||||
{
|
||||
id: 1,
|
||||
value: 9,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 15,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("calculates adventure points cache if a specific rating and bound adventure points are provided", () => {
|
||||
assert.deepEqual<Rated>(
|
||||
helpers.create(1, 9, { boundAdventurePoints: [ { rating: 9, adventurePoints: 10 } ] }),
|
||||
{
|
||||
id: 1,
|
||||
value: 9,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 15,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [
|
||||
{ rating: 9, adventurePoints: 10 },
|
||||
],
|
||||
},
|
||||
)
|
||||
assert.deepEqual<Rated>(
|
||||
helpers.create(1, 9, { boundAdventurePoints: [ { rating: 8, adventurePoints: 10 } ] }),
|
||||
{
|
||||
id: 1,
|
||||
value: 9,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 5,
|
||||
bound: 10,
|
||||
},
|
||||
boundAdventurePoints: [
|
||||
{ rating: 8, adventurePoints: 10 },
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateRating", () => {
|
||||
const oldEntry: Rated = {
|
||||
id: 1,
|
||||
value: 8,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [],
|
||||
}
|
||||
|
||||
const oldEntryWithBound: Rated = {
|
||||
id: 1,
|
||||
value: 8,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [
|
||||
{ rating: 8, adventurePoints: 10 },
|
||||
],
|
||||
}
|
||||
|
||||
it("returns updated rating and adventure points cache", () => {
|
||||
assert.deepEqual<Rated>(
|
||||
helpers.updateValue(oldRating => oldRating + 1, oldEntry),
|
||||
{
|
||||
id: 1,
|
||||
value: 9,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 15,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [],
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual<Rated>(
|
||||
helpers.updateValue(oldRating => oldRating + 1, oldEntryWithBound),
|
||||
{
|
||||
id: 1,
|
||||
value: 9,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 5,
|
||||
bound: 10,
|
||||
},
|
||||
boundAdventurePoints: [
|
||||
{ rating: 8, adventurePoints: 10 },
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("rating", () => {
|
||||
const oldEntry: Rated = {
|
||||
id: 1,
|
||||
value: 9,
|
||||
dependencies: [],
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
boundAdventurePoints: [],
|
||||
}
|
||||
|
||||
it("returns the rating if an entry is present", () => {
|
||||
assert.equal(helpers.value(oldEntry), 9)
|
||||
})
|
||||
|
||||
it("returns the initial rating if no entry is present", () => {
|
||||
assert.equal(helpers.value(undefined), 8)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,270 @@
|
||||
import { ActivatableIdentifier, SkillWithEnhancementsIdentifier } from "optolith-database-schema/types/_IdentifierGroup"
|
||||
import { ImprovementCost } from "./adventurePoints/improvementCost.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 =
|
||||
| {
|
||||
tag: "Minimum"
|
||||
minimum: number
|
||||
}
|
||||
| {
|
||||
tag: "Maximum"
|
||||
maximum: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a dependency on a certain rated entry.
|
||||
*/
|
||||
export type Dependency = {
|
||||
/**
|
||||
* The source of the dependency.
|
||||
*/
|
||||
source: ActivatableIdentifier | SkillWithEnhancementsIdentifier
|
||||
|
||||
/**
|
||||
* If the source prerequisite targets multiple entries, the other entries are
|
||||
* listed here.
|
||||
*/
|
||||
otherTargets: ActivatableIdentifier | SkillWithEnhancementsIdentifier
|
||||
|
||||
/**
|
||||
* The required value.
|
||||
*/
|
||||
value: ValueRestriction
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance of an entry that is specified by a rating/value.
|
||||
*/
|
||||
export type Rated = {
|
||||
/**
|
||||
* The rated entry's identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* The current value.
|
||||
*/
|
||||
value: number
|
||||
|
||||
/**
|
||||
* The accumulated used adventure points value of all value increases.
|
||||
*/
|
||||
cachedAdventurePoints: RatedAdventurePointsCache
|
||||
|
||||
/**
|
||||
* The list of dependencies.
|
||||
*/
|
||||
dependencies: Dependency[]
|
||||
|
||||
/**
|
||||
* A list of bound adventure points. Bound adventure points are granted by the
|
||||
* GM and can only be spent on the entry. They don’t effect the costs of the
|
||||
* rating at the time of granting, so the rating at which they have been
|
||||
* granted is stored as well.
|
||||
*/
|
||||
boundAdventurePoints: BoundAdventurePoints[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create some helper functions for rated entries.
|
||||
*/
|
||||
export const createRatedHelpers = (config: {
|
||||
minValue: number
|
||||
getImprovementCost: (id: number) => ImprovementCost
|
||||
}) => {
|
||||
const { minValue, getImprovementCost } = config
|
||||
|
||||
const updateCachedAdventurePoints = (entry: Rated): Rated => ({
|
||||
...entry,
|
||||
cachedAdventurePoints:
|
||||
cachedAdventurePoints(
|
||||
entry.value,
|
||||
minValue,
|
||||
entry.boundAdventurePoints,
|
||||
getImprovementCost(entry.id),
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
/**
|
||||
* Creates a new entry with an initial value if active. The initial
|
||||
* adventure points cache is calculated from the initial value.
|
||||
*/
|
||||
create: (
|
||||
id: number,
|
||||
value: number = minValue,
|
||||
{
|
||||
dependencies = [],
|
||||
boundAdventurePoints = [],
|
||||
}: Partial<{
|
||||
dependencies: Dependency[]
|
||||
boundAdventurePoints: BoundAdventurePoints[]
|
||||
}> = {},
|
||||
): Rated =>
|
||||
updateCachedAdventurePoints({
|
||||
id,
|
||||
value: Math.max(minValue, value),
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
dependencies,
|
||||
boundAdventurePoints,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update the value with an updater function. This also recalculates the
|
||||
* adventure points cache.
|
||||
*/
|
||||
updateValue: (updater: (oldValue: number) => number, entry: Rated): Rated =>
|
||||
updateCachedAdventurePoints({
|
||||
...entry,
|
||||
value: Math.max(minValue, updater(entry.value)),
|
||||
}),
|
||||
|
||||
/**
|
||||
* Takes an entry that may not exist (because its instance has not been used
|
||||
* yet) and returns its value.
|
||||
*/
|
||||
value: (entry: Rated | undefined): number => entry?.value ?? minValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance of an activatable entry that is specified by a rating/value.
|
||||
*/
|
||||
export type ActivatableRated = {
|
||||
/**
|
||||
* The rated entry's identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* The current value, if activated.
|
||||
*/
|
||||
value?: number
|
||||
|
||||
/**
|
||||
* The accumulated used adventure points value of all value increases.
|
||||
*/
|
||||
cachedAdventurePoints: RatedAdventurePointsCache
|
||||
|
||||
/**
|
||||
* The list of dependencies.
|
||||
*/
|
||||
dependencies: Dependency[]
|
||||
|
||||
/**
|
||||
* A list of bound adventure points. Bound adventure points are granted by the
|
||||
* GM and can only be spent on the entry. They don’t effect the costs of the
|
||||
* rating at the time of granting, so the rating at which they have been
|
||||
* granted is stored as well.
|
||||
*/
|
||||
boundAdventurePoints: BoundAdventurePoints[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create some helper functions for rated entries.
|
||||
*/
|
||||
export const createActivatableRatedHelpers = (config: {
|
||||
getImprovementCost: (id: number) => ImprovementCost
|
||||
}) => {
|
||||
const { getImprovementCost } = config
|
||||
const minValue = 0
|
||||
|
||||
const updateCachedAdventurePoints = (entry: ActivatableRated): ActivatableRated => ({
|
||||
...entry,
|
||||
cachedAdventurePoints:
|
||||
cachedAdventurePointsForActivatable(
|
||||
entry.value,
|
||||
entry.boundAdventurePoints,
|
||||
getImprovementCost(entry.id),
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
/**
|
||||
* Creates a new entry with an initial value if active. The initial
|
||||
* adventure points cache is calculated from the initial value.
|
||||
*/
|
||||
create: (id: number, value?: number): ActivatableRated =>
|
||||
updateCachedAdventurePoints({
|
||||
id,
|
||||
value: value === undefined ? undefined : Math.max(minValue, value),
|
||||
cachedAdventurePoints: {
|
||||
general: 0,
|
||||
bound: 0,
|
||||
},
|
||||
dependencies: [],
|
||||
boundAdventurePoints: [],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update the value with an updater function. This also recalculates the
|
||||
* adventure points cache.
|
||||
*/
|
||||
updateValue: (
|
||||
updater: (oldValue: number | undefined) => number | undefined,
|
||||
entry: ActivatableRated
|
||||
): ActivatableRated => {
|
||||
const newValue = updater(entry.value)
|
||||
|
||||
return updateCachedAdventurePoints({
|
||||
...entry,
|
||||
value: newValue === undefined ? undefined : Math.max(minValue, newValue),
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes an entry that may not exist (because its instance has not been used
|
||||
* yet) and returns its value.
|
||||
*/
|
||||
value: (entry: ActivatableRated | undefined): number | undefined => entry?.value,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance of an activatable entry that is specified by a rating/value.
|
||||
*/
|
||||
export type ActivatableRatedWithEnhancements = {
|
||||
/**
|
||||
* The rated entry's identifier.
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* The current value, if activated.
|
||||
*/
|
||||
value?: number
|
||||
|
||||
/**
|
||||
* The accumulated used adventure points value of all value increases.
|
||||
*/
|
||||
cachedAdventurePoints: RatedAdventurePointsCache
|
||||
|
||||
/**
|
||||
* The list of dependencies.
|
||||
*/
|
||||
dependencies: Dependency[]
|
||||
|
||||
/**
|
||||
* A list of bound adventure points. Bound adventure points are granted by the
|
||||
* GM and can only be spent on the entry. They don’t effect the costs of the
|
||||
* rating at the time of granting, so the rating at which they have been
|
||||
* granted is stored as well.
|
||||
*/
|
||||
boundAdventurePoints: BoundAdventurePoints[]
|
||||
|
||||
/**
|
||||
* The currently active enhancements for that entry.
|
||||
*/
|
||||
enhancements: {
|
||||
[id: number]: Enhancement
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
--print-border: 0.25mm solid black;
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
.theme--light {
|
||||
--selection-color: #c2ad88;
|
||||
--accent-color-intense: #b6a68a;
|
||||
--background-color: #f0f0f0;
|
||||
@@ -72,6 +72,6 @@
|
||||
// --strength-color: #ea8c00;
|
||||
}
|
||||
|
||||
.platform-darwin {
|
||||
.platform--darwin {
|
||||
--titlebar-height: 36px;
|
||||
}
|
||||
|
||||
@@ -65,3 +65,16 @@ p {
|
||||
text-transform: uppercase;
|
||||
color: var(--headings-color);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 15px 0 5px;
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: var(--separator-color-transparent);
|
||||
|
||||
&.vertical {
|
||||
margin: 0 20px;
|
||||
height: auto;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { OutputSelector, createSelector } from "@reduxjs/toolkit"
|
||||
|
||||
type Indexed = { [key: string | number]: any }
|
||||
|
||||
export const createPropertySelector = <S, O extends Indexed, K extends keyof O>(
|
||||
selector: (state: S) => O,
|
||||
property: K,
|
||||
): OutputSelector<[typeof selector], O[K] | undefined, (obj: O) => O[K] | undefined> =>
|
||||
createSelector(selector, obj => obj[property])
|
||||
@@ -14,6 +14,5 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="modals-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user