refactor: migrate attributes tab except attribute adjustment component

This commit is contained in:
Lukas Obermann
2023-06-11 22:16:23 +02:00
parent 44357e1387
commit beb839da5d
66 changed files with 4387 additions and 2003 deletions
+5 -2
View File
@@ -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
-391
View File
@@ -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)
}
))
)
-26
View File
@@ -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)
)
-213
View File
@@ -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="&#xE908;"
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="&#xE909;"
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="&#xE908;"
onClick={handleAdd}
disabled={
(isInCharacterCreation && sum >= Maybe.sum (maxTotalAttributeValues))
|| or (fmapF (mmax) (lte (value)))
}
/>
{isRemovingEnabled
? (
<IconButton
className="remove"
icon="&#xE909;"
onClick={handleRemove}
disabled={value <= AWRA.min (attr)}
/>
)
: null}
</AttributeBorder>
)
}
-181
View File
@@ -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="&#xE90c;"
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="&#xE908;"
onClick={handleOpenAddPermanentEnergyLoss}
/>
)}
<AttributesRemovePermanent
remove={addLostPoints}
staticData={staticData}
isOpen={Maybe.elem (id) (getAddPermanentEnergy)}
close={closeAddPermanentEnergyLoss}
/>
{!isRemovingEnabled && isFunction (addBoughtBackPoint)
? (
<IconButton
className="remove"
icon="&#xE909;"
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="&#xE908;"
onClick={addBoughtBackPoint}
disabled={fromJust (permanentBoughtBack) >= permanentSpent}
/>
<IconButton
className="remove"
icon="&#xE909;"
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="&#xE908;" onClick={addLostPoint} />
<IconButton
className="remove"
icon="&#xE909;"
onClick={removeLostPoint}
disabled={permanentSpent <= 0}
/>
</div>
</div>
</div>
</Dialog>
)
}
-161
View File
@@ -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>
)
}
+10
View File
@@ -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 }
}
+11
View File
@@ -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
}
+11 -5
View File
@@ -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,
}
}
+2
View File
@@ -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>
)
}
+35 -52
View File
@@ -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="&#xE90f;"
onClick={undo}
disabled={!isUndoAvailable}
label={translate("Undo")}
// onClick={undo}
disabled/* ={!isUndoAvailable} */
/>
<IconButton
icon="&#xE910;"
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="&#xE906;"
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;
+6 -6
View File
@@ -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}
+2 -1
View File
@@ -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>
// )
// }
@@ -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="&#xE908;"
onClick={handleAdd}
disabled={!isIncreasable}
label={translate("Increment")}
/>
{isRemovingEnabled
? (
<IconButton
className="remove"
icon="&#xE909;"
onClick={handleRemove}
disabled={!isDecreasable}
label={translate("Decrement")}
/>
)
: null}
</AttributeBorder>
)
}
@@ -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>
)
}
@@ -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="&#xE908;"
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="&#xE909;"
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>
)
}
@@ -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="&#xE90c;"
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="&#xE908;"
label={translate("Lost Total")}
onClick={openPermanentLossSheet}
/>
<PermanentLossSheet
remove={handleAddPermanentlyLostPoints}
isOpen={isPermanentLossSheetOpen}
close={closePermanentLossSheet}
/>
</>
)}
{!isRemovingEnabled && id !== DCId.LifePoints
? (
<IconButton
className="remove"
icon="&#xE909;"
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="&#xE908;"
label={translate("Increment")}
onClick={addBoughtBackPoint}
disabled={permanentlyLostBoughtBack >= permanentlyLost}
/>
<IconButton
className="remove"
icon="&#xE909;"
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="&#xE908;"
label={translate("Increment")}
onClick={addLostPoint}
/>
<IconButton
className="remove"
icon="&#xE909;"
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]
)
+56
View File
@@ -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)
}
})
+809
View File
@@ -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)
})
+35
View File
@@ -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
+156
View File
@@ -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--
}
})
+1 -1
View File
@@ -34,7 +34,7 @@ type RouteState = {
}
const initialRouteState: RouteState = {
route: "characters",
route: "attributes",
}
const routeSlice = createSlice({
+8 -1
View File
@@ -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
+2
View File
@@ -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,
},
+14
View File
@@ -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
+39
View File
@@ -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)
@@ -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 dont 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 its 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 its 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)
}
}
+24
View File
@@ -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[]
}
+99
View File
@@ -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
}
+157
View File
@@ -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)
})
})
})
+270
View File
@@ -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 dont 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 dont 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 dont 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
}
}
+2 -2
View File
@@ -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;
}
+13
View File
@@ -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;
}
}
+9
View File
@@ -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])
-1
View File
@@ -14,6 +14,5 @@
</head>
<body>
<div id="root"></div>
<div id="modals-root"></div>
</body>
</html>