diff --git a/libs/gi/formula/doc/tag_arch.md b/libs/gi/formula/doc/tag_arch.md new file mode 100644 index 0000000000..93f9a9dd17 --- /dev/null +++ b/libs/gi/formula/doc/tag_arch.md @@ -0,0 +1,171 @@ +# Tag Architecture + +Tag categories in this module identifies the calculation as follows + +- `name:` the top-level formula +- `preset:` TODO _Do we remove this?_ +- `src:` the character that is applying buffs +- `dst:` the character that is receiving buffs +- `sheet:` the sheet that contains the formula +- `et:` the entry type +- `qt:` and `q:` the query + +- `region:` the region of the character +- `move:` the type of the current dmg formula +- `ele:` the element of the current formula +- `trans:` the transformative reaction +- `amp:` the amplifying reaction +- `cata:` the catalytic reaction + +String representation of a tag (computed using `tagStr`) is of the form + +``` +{ #{name} {preset} {src} ({dst}) {sheet} {et} {qt}.{q} | {region} {move} {ele} {trans} {amp} {cata} } +``` + +Missing tags are omitted. + +## Query + +The current computation, called query, is identified by `qt: q:`. +It is always set when performing a `read` operation, and valid query combinations are specified in `data/util/tag.ts`. +All queries assume a certain `et: sheet:` and `accu` upon read, which is specified by `Desc` and enforced via `convert` (both declared in `tag.ts`). +As an example, to calculate the current character's skill talent level, use `read({ et:self qt:char q:skill sheet:agg }, 'sum')` or simply `self.char.skill`. +For more details on `et:` and `sheet:` see below. + +We split query identifier into `qt:` and `q:` to simplify formula specifications. +Some formulas apply to a large group of queries, most of which having a common `qt:` by design, e.g., +all `qt:premod` stats include the value from `qt:base` stats. + +## Top-level Formulas and Prep Phase + +Top-level formulas are queries that are used directly by the UI, e.g., character's dmg formula. +They are identified by `qt:formula`, and are declared in [prep.ts](../src/data/common/prep.ts). +We assume the following tags (in addition to `qt:formula`) to exist for the top-level formulas, + +- `q:dmg`: `et:self sheet: name: move:`, +- `q:heal`: `et:self sheet: name:`, +- `q:shield`: `et:self sheet: name: ele:`, +- `q:trans/swirl`: `et:self sheet: name: ele: trans:`. + +Most of these formulas begin by preparing appropriate tags for the rest of the formulas. +Tags (such as dmg element `ele:`) are assumed to exist throughout the formula specification, but computing the correct value requires a calculator. +So it cannot be set prior to calculator creation. +Instead, we calculate them while the tags are not assumed ready, hence the `prep` phase, identified by `qt:prep`. +In this phase, the formulas are more restricted in the avilable tags (e.g., `qt:prep q:ele` cannot use `ele:`). +Once `prep:` calculation is completed, the tags are attached to the base formula via `dynTag`. + +## Entry Type and Sheet Specifier + +The tag categories `et:` and `sheet:` are separated into read-side, which is used by `read` operations, and write-side, which is used as tags in tag database entries. + +- Read-side `et:` specifies whether the query computes + - The current character stat (`et:self`), + - Team-wide stat (`et:team`), + - The stat of the buff target (`et:target`), or + - The common enemy stat (`et:enemy`). +- Write-side `et:` specifies whether the entry applies + - To the current character (`et:self`), + - To the entire team (`et:teamBuff`, inside sheets only), + - To other members (`et:notSelfBuff`, inside sheets only), or + - To the (common) enemy (`et:enemyDeBuff` inside sheets and `et:enemy` outside sheets). +- Read-side `sheet:` speficies the sheets to include in gathering, whether to gather + - All sheets from all members (`sheet:agg`), + - Only character sheets of the current member (`sheet:iso`), or + - Common listing outside any specific sheets (`static`). +- Write-side `sheet:` specifies + - The sheet the entry belongs to (`sheet://`), or + - That the entry is a UI custom formula (`sheet:custom`). + +Note that some tags are both read- and write-sides. +Every query starts with a read-side `sheet: et:` combination. +The gathering operation then maps to the appropriate write-side `sheet: et:` via util functions. +Following is the gathered entries on different `sheet: et:` combinations: + +- `sheet:agg et:self` queries (e.g., `self.char.skill`) + - Non-specific non-sheet contributions + - `{ sheet:agg et:self } <= { sheet:custom }` from `data/common/index.ts` + - Custom contributions + - `{ sheet:agg } <= { sheet: }` from `char/weapon/artData` with `withMember` + - Sheet-specific `et:self` contributions from appropriate members + - (Artifact only) `{ sheet:art qt:premod } <= { sheet:dyn }` + - Hook for conversion to untagged graph. + - `{ src: sheet:agg } <= { src:* dst: et:teamBuff/notSelfBuff }` from `teamData` + - `{ src: sheet:agg } <= { sheet: }` from `char/weapon/artData` with `withMember` + - Sheet-specific `et:*Buff` contributions from appropriate members `src: dst:` +- `sheet:iso et:self` queries (e.g., `self.char.lvl`) + - Non-specific non-sheet contributions + - `{ sheet:iso et:self } <= { sheet:custom }` from `data/common/index.ts` + - Custom contributions + - `{ sheet:iso } <= { sheet: }` from `charData` with `withMember` + - Char-sheet-specific `et:self` contributions from the current character +- `sheet:agg et:enemy` queries (e.g., `enemy.common.defIgn`) + - `{ src: sheet:agg } <= { src:* dst: et:enemyDeBuff }` from `teamData` + - `{ src: sheet:agg } <= { sheet: }` from `char/weapon/artData` with `withMember` + - Sheet-specific contributions from appropriate members `src: dst:` and write-side `sheet: et:` +- `sheet:iso et:enemy` queries (unused so far) + - `{ sheet:iso } <= { sheet: }` from `charData` with `withMember` + - Char-sheet-specific contributions from the current character +- `sheet:static et:self/enemy` (e.g., `self.commin.critMode`) + - Non-sheet contributions from the same tag +- `et:team sheet:agg/iso` queries (e.g., `team.final.atk`) + - `{ et:team } <- { src:* et:self }` (`teamData`) + - `et:self` query from each member (with the same `sheet:`) + +Notes: + +- Write-side `et:*Buff` can only be used inside a sheet as they require the `teamData` entry to insert `src:` and `char/weapon/artData` (with `withMember`) to override `sheet:`. + Outside of a sheet, `et:self/enemy` must be used instead as the `teamData` entry overrides `et:` in the process. + - The convention is to use `selfBuff/teamBuff/notSelfBuff/enemyDebuff` _variables_ for all entry creations, and "fix" the `et:` for sheets when it is `register`ed. +- Artifact use `sheet:art` instead of `sheet:` as all artifacts are always included together. + This helps reduce the `read` needed due to the sheer number of artifacts. + - `sheet:` is used only for counting the number equipped artifact of that set. + +## Conditionals + +Conditional query tags are of the form + +``` +{ + et: 'self', qt: 'cond', // Fixed tags + sheet:, q:, // Conditional identifier + src:, // Character that is applying (src:) buff + dst:, // Character that is receiving (dst:) buff + // Unused tags + name:null, region:null, ele:null, move:null, + trans:null, amp:null, cata:null +} +``` + +Since the tag requires both `src:` and `dst:`, conditionals are only valid when both are guaranteed to exist. +A notable class of entries that satisfy the condition are entries with `et:selfBuff/teamBuff/notSelfBuff/enemyDebuff` tags. +We call those entries _buff context_, as those entries are missing when calculating stats without team information. + +> Unused tags are set to `null` when reading conditionals to improve caching. + +## Optional Tags + +Tags `name: region: move: ele: trans: amp: cata:` (`name:` and everything on the right of `|` in the string representation) are generally not assumed to be present in most formulas. +Instead, it is used to include additions to the current formula. +For example, reading `{ q:dmg_ ele:hydro }` gathers both any-element dmg bonus (`{ q:dmg_ }`) and hydro-only dmg bonus (`{ q:dmg_ ele:hydro }`). +Most formulas also retain these optional tags, so the specified optional tags at a top-level formula also apply to the deeper parts of the formula, e.g., character talent level. + +> `name:null` is applied when crossing team buff boundaries to improve caching. + +## Mechanisms + +### Formula Listing + +TODO + +### Non-Stacking (`Read.addOnce`) + +TODO + +### Priority String + +TODO + +### `meta.ts` generation + +TODO diff --git a/libs/pando/engine/README.md b/libs/pando/engine/README.md index d03b821d44..9ff887382c 100644 --- a/libs/pando/engine/README.md +++ b/libs/pando/engine/README.md @@ -1,11 +1,126 @@ -# pando +# Pando -This library was generated with [Nx](https://nx.dev). +## Usage -## Building +TODO -Run `nx build pando` to build the library. +## Tags -## Running unit tests +A tag is a dictionary of tag category (key) and tag value (value) pairs. +We denote a tag category and a tag value by `cat:` and `cat:val`, respectively. +When there is no ambiguity, `val` may be used instead of `cat:val`. +We also define a "combination" of tags `T1` and `T2`, denoted by `T1/T2`, as a tag such that for any tag category `cat:`, -Run `nx test pando` to execute the unit tests via [Jest](https://jestjs.io). +- If `cat:v` ∈ `T2`, then `cat:v` ∈ `T1/T2`, +- If `cat:` ∉ `T2` and `cat:v` ∈ `T1`, then `cat:v` ∈ `T1/T2`, and +- If `cat:` ∉ `T2` and `cat:` ∉ `T1`, then `cat:` ∉ `T1/T2`. + +Note the asymmetry between `T1` and `T2`. +As an example, the combination `{ c1:v1 c2:v2 }/{ c2:v3 c3:v4 }` is the tag `{ c1:v1 c2:v3 c3:v4 }`. +In this example, `c1:` and `c3:` exist in only one of the tags, and so their values are used. +For `c2:`, both tags contain different values, and so the value in the right tag is preferred. + +### Tag Database Gathering + +A calculator `calc` contains an array of tag-node pairs, called Tag Database. +Each entry `{ tag, value }` in the tag database signifies that the computation of `tag` should include `value`. +We denote a tag database entry with `tag <- value`. +If `value` is a reread to `tag2`, we may instead denote the entry with `tag <= tag2`. +Note the different arrow type between the two, as well as the type difference on the right side of the arrow. + +With tag database, `calc` can _gather a tag `T`_ via `calc.get(T)`, returning all entries in the tag database with matching tags. +An entry `tag <- value` in the tag database is included in a gathering iff for every `k:v` ∈ `tag`, `v == null` or `k:v` ∈ `T`. +If the value in the included entry is a `node`, its value is computed using tag `T`. +If the value is a `Reread` with tag `T2`, another gather is performed using `T/T2`, and its result is appended to the final result. +As an example, consider `calc.get({ c1:v1 c2:vA })` when the `calc`ulator has the following Tag Database, + +``` +[ + { c1:v1 } <- node1, // entry 1 + { c1:v2 } <- node2, // entry 2 + { c1:v1 c2:vA } <- node3, // entry 3 + { c1:v1 c2:vB } <- node4, // entry 4 + { c2:vA } <- node5, // entry 5 + { c1:v1 c2:vA } <= { c2:vB } // entry 6 +]. +``` + +In this case, `calc` first selects the matching entries 1, 3, 5, and 6. +As entries 1, 3, and 5 contain nodes, `calc` computes nodes 1, 3, and 5 with tag `{ c1:v1 c2:vA }`. +Next, the the calculator resolves entry 6, by performing a gathering with tag `{ c1:v1 c2:vA }/{ c2:vB } = { c1:v1 c2:vB }`, computing nodes 1 and 4 with tag `{ c1:v1 c2:vB }`. +The calculator then returns the following: + +- Value of `node1` computed with tag `{ c1:v1 c2:vA }`, +- Value of `node3` computed with tag `{ c1:v1 c2:vA }`, +- Value of `node5` computed with tag `{ c1:v1 c2:vA }`, +- Value of `node1` computed with tag `{ c1:v1 c2:vB }`, and +- Value of `node4` computed with tag `{ c1:v1 c2:vB }`. + +Note that `node1` is computed twice, each with different tags, due to `reread` operation. + +## Node Operations + +This section outlines all operations supported by Pando. +Operations are separated into three types, arithmetic, branching, and tag-related. + +### Arithmetic Operations + +- `constant(c)`: values of a constant `c` (converting it to a `Node`), + - This is normally unneeded as most functions permit both `Node` and constants, +- `sum(x1, x2, ...) := x1 + x2 + ...`, +- `prod(x1, x2, ...) := x1 * x2 * ...`, +- `min(x1, x2, ...) := Math.min(x1, x2, ...)`, +- `max(x1, x2, ...) := Math.max(x1, x2, ...)`, +- `sumfrac(x1, x2) := x1 / (x1 + x2)`, +- `subscript(index, array) := array[index]`. + `array` can be either array of strings or of numbers, +- `custom(op, x1, x2, ...)` is for custom node for non-standard computations, + - See [Calculator Customization Section](#customize) on how to add support for custom operations. + +### Branching Operations + +Most branching functions are of the form `cmp<>(x1, x2, pass, fail)` where a comparator CMP (e.g., `Eq`) is used to compare `x1` and `x2`. +If the comparison yields true (e.g., `x1 == x2` for `cmpEq`), then `pass` branch is chosen. +Otherwise, `fail` branch is chosen. +Unchosen branch is not evaluated. +`fail` can be omitted if it is 0. +Supported comparators include + +- `Eq` (`x1 == x2`) and `NE` (`x1 != x2`), +- `GE` (`x1 >= x2`) and `GT` (`x1 > x2`), and +- `LE` (`x1 <= x2`) and `LT` (`x1 < x2`). + +Another branching function is `lookup(key, table, defaultV) := table[key] ?? defaultV`. +There are two main distinctions between `subscript` and `lookup`: + +- `lookup` indices are strings while `subscript` indices are numbers, and +- `lookup` `table` may contain complex nodes while `subscript` `array` can contain only constants. + +Nodes other than `table[key]` are not evaluated. +When both `lookup` and `subscript` are applicable, prefer `subscript` for performance reason. + +### Tag-Related Operations + +By default, all arithmetic and branching operations preserve the tags, e.g., calculating `sum(x1, x2)` with a tag `T`, the calculation of `x1` and `x2` also use the same tag `T`. +When computing with a current tag `Tcur` + +- `tagVal(cat)` reads the value of `Tcur` at category `cat`, or `""` if `cat:` ∉ `Tcur`, +- `tag(v, tag)` calculates `v` using `Tcur/tag`, +- `dynTag(v, tag)` calculates `v` using `Tcur/tag`. + The main difference compared to `tag` operation is that the tag values in `dynTag` can be other nodes, which are computed with `Tcur` tag. + When both `dynTag` and `tag` are applicable, prefer `tag` for performance reason. +- `read(tag, accu)` performs a gather with tag `Tcur/tag`, then combine the results using `accu`mulator the accumulators include `sum/prod/min/max`, corresponding to the arithmetic operations. + Accumulator may be `undefined`, in which case, the gathering is assumed to contain exactly one entry. + +## Calculator Customization + +`Calculator` can be customized via subclassing. +Functions that are designed to be overriden by such subclasses include + +``` +- computeMeta(n: AnyNode, value: number | string, + x: (CalcResult | undefined)[], + br: CalcResult[], + tag: Tag | undefined): M +- computeCustom(args: (number | string)[], op: string): any +```