makes topdata json validation much stricter, currently still too permissive Reviewed-on: #24 Reviewed-by: xtul <mpiasecki720@protonmail.com> Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
104 lines
3.3 KiB
Markdown
104 lines
3.3 KiB
Markdown
# Class Feat Global Injection Contract
|
|
|
|
## Status Snapshot
|
|
|
|
Current state as of 2026-06-05:
|
|
|
|
- Preferred class feat injection policy is authored in
|
|
`topdata/data/classes/feats/global.json`.
|
|
- `global.json` rows flow through the ordinary class feat expansion pipeline,
|
|
including successor expansion, masterfeat expansion, class-skill filtering,
|
|
reference resolution, deduplication, and final 2DA emission.
|
|
- Legacy `topdata.class_feat_injections` YAML still loads only as a
|
|
compatibility path when no applicable class feat `global.json` exists.
|
|
- Toolkit hardcoded defaults are compatibility-only and must not be the normal
|
|
source of project policy.
|
|
|
|
## Objective
|
|
|
|
Native class feat generation must produce deterministic, legacy-equivalent
|
|
effective output while keeping project policy in topdata authoring files.
|
|
|
|
## Requirements
|
|
|
|
- For each class skill, inject the configured skill-focus masterfeat rows from
|
|
`classes/feats/global.json` using `FeatIndex.filter = "classskills"`.
|
|
- Inject `feat:literate` only when the class table does not already include
|
|
`feat:illiterate`.
|
|
- Inject the shared combat/menu feat rows declared in the manifest unless a
|
|
class already authors the equivalent `FeatIndex`.
|
|
- Explicit class feat rows take precedence over injected rows.
|
|
- Injections must be deterministic for identical inputs.
|
|
|
|
## Manifest Shape
|
|
|
|
```json
|
|
{
|
|
"position": "prepend",
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"FeatIndex": { "id": "feat:literate" },
|
|
"GrantedOnLevel": 1,
|
|
"List": 3,
|
|
"OnMenu": 0
|
|
},
|
|
"unless_present": [
|
|
{ "field": "FeatIndex", "id": "feat:illiterate" }
|
|
]
|
|
},
|
|
{
|
|
"row": {
|
|
"FeatIndex": {
|
|
"id": "masterfeats:skill_focus",
|
|
"filter": "classskills"
|
|
},
|
|
"GrantedOnLevel": -1,
|
|
"List": 1,
|
|
"OnMenu": 0
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`position` defaults to `append`. The module uses `prepend` for class feat
|
|
injections to keep generated policy rows before per-class authored rows.
|
|
|
|
## Global Injection Grammar
|
|
|
|
Each injection entry may use only `row`, `require_present`, `any_present`, and
|
|
`unless_present`. `when_present` is invalid and rejected; it is not an alias.
|
|
|
|
```json
|
|
{
|
|
"row": {
|
|
"FeatIndex": { "id": "feat:example" }
|
|
},
|
|
"require_present": [{ "field": "FeatIndex", "id": "feat:required" }],
|
|
"any_present": [
|
|
{ "field": "FeatIndex", "id": "masterfeats:metamagic" },
|
|
{ "field": "FeatIndex", "id": "masterfeats:combat" }
|
|
],
|
|
"unless_present": [{ "field": "FeatIndex", "id": "feat:example" }]
|
|
}
|
|
```
|
|
|
|
- `require_present` means every listed reference must exist.
|
|
- `any_present` means at least one listed reference must exist, and the array
|
|
must not be empty.
|
|
- `unless_present` means none of the listed references may exist.
|
|
- Every present group on an entry must pass.
|
|
- Conditions observe the current rows in injection order, so earlier injections
|
|
can affect later conditions.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- Class feat output contains the same effective shared feats as the prior YAML
|
|
policy.
|
|
- Literacy injection is skipped for illiterate classes.
|
|
- Skill-focus masterfeat rows expand only for class skills.
|
|
- YAML compatibility continues to work for consumers that have not migrated.
|
|
- A project with `classes/feats/global.json` does not also receive YAML or
|
|
hardcoded default class feat injections.
|