Support for global.json files

This commit is contained in:
2026-06-05 10:36:47 +02:00
parent d0e065536c
commit c672a1ff33
5 changed files with 804 additions and 264 deletions
@@ -1,207 +1,76 @@
# Global Feat Injection — Specification
# Class Feat Global Injection Contract
## Status Snapshot
Current state as of 2026-05-24:
Current state as of 2026-06-05:
- Current code reads class feat injection policy from
`topdata.class_feat_injections` in `nwn-tool.yaml`.
- `global_feats` injects concrete `feat:*` references into each
`classes/feats/*.json` table, unless the class already authors the same feat.
- `global_feats` supports `require_present` and `unless_present` conditions
against other feat references already present in the class feat table.
- `class_skill_masterfeats` expands configured `masterfeats:*` rows once for
each class skill.
- If a project omits `topdata.class_feat_injections`, the toolkit keeps the
former hardcoded default injections for compatibility.
- 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
Ensure the native class feat generation pipeline produces **functionally identical output** to the legacy system by applying deterministic global feat injections.
---
Native class feat generation must produce deterministic, legacy-equivalent
effective output while keeping project policy in topdata authoring files.
## Requirements
### R1 — Skill-based master feats
- 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.
For every **class skill**, inject:
- `masterfeats:skillfocus`
- `masterfeats:greaterskillfocus`
With properties:
````json
{
"GrantedOnLevel": "-1",
"List": "1",
"OnMenu": "0"
}
For the current implementation, `masterfeats:greaterskillfocus` is injected with
`GrantedOnLevel = 12`, not `-1`.
---
### R2 — Spellcasting master feats
If class core defines:
- `"SpellCaster" = 1`
Then inject:
- `masterfeats:spellfocus`
- `masterfeats:greaterspellfocus`
Historical note:
- This rule is not currently implemented as an automatic global injection in
`native.go`.
- Spell-focus expansion is supported when the class feat data explicitly
references `masterfeats:spellfocus`.
If automatic spellcaster-driven injection is still required, it remains
outstanding work.
With properties:
## Manifest Shape
```json
{
"GrantedOnLevel": "-1",
"List": "1",
"OnMenu": "0"
}
````
---
### R3 — Literacy rule
Inject:
- `feat:literate`
**Only if**:
- `feat:illiterate` is NOT present
With properties:
```json
{
"GrantedOnLevel": "1",
"List": "3",
"OnMenu": "0"
"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
}
}
]
}
```
---
### R4 — Default combat/menu feats
Always inject:
- `feat:special_attacks`
- `feat:throw`
- `feat:grapple`
- `feat:offensive_fighting`
- `feat:defensive_fighting`
- `feat:horse_menu`
With properties:
```json
{
"GrantedOnLevel": "1",
"List": "3",
"OnMenu": "1"
}
```
---
### R5 — Conditional combat feats
Inject:
- `feat:power_attack`
- `feat:combat_expertise`
With properties:
```json
{
"GrantedOnLevel": "-1",
"List": "0",
"OnMenu": "1"
}
```
---
### R6 — Override precedence
- Explicitly authored class feat rows take precedence over injected rows.
- Injection MUST NOT replace explicit class feat data.
- Dataset overrides are applied before class feat injection in the native
dataset merge pipeline; author a class feat row directly to override or
suppress the injected equivalent for a class.
---
## Behavioral Rules
### B1 — Output equivalence
- The resulting feat set MUST match legacy output **in substance**.
- Exact row ordering and numeric IDs are NOT significant.
### B2 — Deterministic inclusion
- All injections MUST occur consistently for identical inputs.
- No randomness or order-dependent behavior is allowed.
### B3 — Conditional correctness
- `feat:literate` MUST NOT be injected if `feat:illiterate` exists.
- Spell focus feats MUST ONLY be injected when `"SpellCaster" = 1`.
### B4 — Integration with pipeline
- Injected feats MUST flow through the same processing stages as authored feats.
- They MUST be subject to:
- overrides
- deduplication
- final emission rules
---
`position` defaults to `append`. The module uses `prepend` for class feat
injections to keep generated policy rows before per-class authored rows.
## Acceptance Criteria
An implementation is considered correct if:
1. **Parity**
- For any given class input, native output produces the same effective feats as legacy.
2. **Injection coverage**
- All required feats (R1R5) appear when conditions are met.
3. **Exclusion correctness**
- No invalid injections occur (e.g., literacy conflict).
4. **Override dominance**
- Any override affecting injected feats is correctly applied.
5. **Stability**
- Repeated runs with identical input produce identical output.
---
## Non-Negotiables
- This is **parity-critical**, not a best-effort feature.
- Native pipeline MUST be capable of fully replacing legacy output.
- There MUST be **zero dependency** on legacy systems at runtime.
- Any deviation from legacy-equivalent output is a **defect**.
- 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.