Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add document to pando and gi-formula #2376

Merged
merged 14 commits into from
Aug 25, 2024
171 changes: 171 additions & 0 deletions libs/gi/formula/doc/tag_arch.md
Original file line number Diff line number Diff line change
@@ -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:<char key>/<weapon key>/<art>`), 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:<char key/weapon key/art> }` 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:<src> sheet:agg } <= { src:* dst:<src> et:teamBuff/notSelfBuff }` from `teamData`
- `{ src:<src> sheet:agg } <= { sheet:<char key/weapon key/art> }` 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:<char key> }` 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:<src> sheet:agg } <= { src:* dst:<src> et:enemyDeBuff }` from `teamData`
- `{ src:<src> sheet:agg } <= { sheet:<char key/weapon key/art> }` 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:<char key> }` 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:<artifact name>` as all artifacts are always included together.
This helps reduce the `read` needed due to the sheer number of artifacts.
- `sheet:<artifact name>` 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:<sheet>, q:<cond name>, // Conditional identifier
src:<src>, // Character that is applying (src:) buff
dst:<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
127 changes: 121 additions & 6 deletions libs/pando/engine/README.md
Original file line number Diff line number Diff line change
@@ -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<<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.

## <a name="customize"></a> 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<number | string, M> | undefined)[],
br: CalcResult<number | string, M>[],
tag: Tag | undefined): M
- computeCustom(args: (number | string)[], op: string): any
```
Loading