dndwright 0.23.2__tar.gz → 0.26.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dndwright-0.23.2 → dndwright-0.26.0}/CHANGELOG.md +60 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/PKG-INFO +2 -1
- {dndwright-0.23.2 → dndwright-0.26.0}/README.md +1 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/pyproject.toml +1 -1
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/__init__.py +20 -1
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/adapters.py +65 -1
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/assembler.py +1 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/character_evaluator.py +11 -2
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/dnd_5e_2024.py +16 -2
- dndwright-0.26.0/src/dndwright/rules/homebrew_validator.py +599 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/operations.py +5 -3
- dndwright-0.26.0/tests/test_adapters_bugs.py +221 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_api_contract.py +19 -1
- dndwright-0.26.0/tests/test_evaluate_character_components.py +69 -0
- dndwright-0.26.0/tests/test_homebrew_validator.py +563 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/.github/workflows/ci.yml +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/.github/workflows/publish.yml +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/.gitignore +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/LICENSE +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/NOTICE +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/RELEASING.md +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/assets/combat-flow.svg +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/assets/compose.svg +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/assets/computation-graph.svg +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/assets/dice.svg +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/assets/overview.svg +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/assets/theme-scaling.svg +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/README.md +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/combat.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/compose.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/conditions.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/content_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/custom_operation.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/dice.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/export_graph.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/feat_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/gated_contributions.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/initiative.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/multiclass.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/quickstart.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/stat_diff.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/examples/weapon_attack.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/cli.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/combat/__init__.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/combat/combat.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/combat/conditions.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/combat/initiative.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/combat/weapons.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/__init__.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/armor.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/backgrounds.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/classes.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/conditions.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/creatures.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/feats.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/generate.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/magic_items.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/models.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/species.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/spells.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/content/weapons.json +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/dice/__init__.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/dice/engine.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/ontology/__init__.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/ontology/dnd.yaml +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/ontology/loader.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/py.typed +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/__init__.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/compose.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/evaluator.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/export.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/lookup_tables.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/schema.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/theme_scaling.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/src/dndwright/rules/validation.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_background_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_backgrounds.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_character_theme_scaling.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_classes.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_cli.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_combat.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_combat_api_contract.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_compose.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_conditions.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_content.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_content_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_content_models.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_creatures.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_defenses_and_serialization.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_dice.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_dice_api_contract.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_engine.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_evaluator_cache.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_examples.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_export.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_feat_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_gated_contributions.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_initiative.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_input_validation.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_ontology.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_operations_registry.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_properties.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_public_graph_api.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_species_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_spell_components.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_spells.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_theme_scaling.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_validation.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_weapon_attack.py +0 -0
- {dndwright-0.23.2 → dndwright-0.26.0}/tests/test_weapons_armor.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `validate_class_homebrew` now accepts the canonical `CharClass` shape: dice-string `hit_die`
|
|
7
|
+
(e.g. `"d10"`) and case-insensitive ability names in saving throws. It previously demanded an
|
|
8
|
+
int `hit_die` + lowercase abilities, false-positiving on every valid generated class — and
|
|
9
|
+
contradicting dndwright's own `CharClass` model (`hit_die: str`). Bare-int hit_die and lowercase
|
|
10
|
+
abilities still validate; genuinely-bad data is still caught.
|
|
11
|
+
|
|
12
|
+
## 0.25.1
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Hit-die compute/display desync: the displayed hit die now matches the archetype/class override used for HP computation (was showing the raw LLM value).
|
|
16
|
+
- Species natural armor now contributes to AC via a `natural_armor_ac` computation-graph input; AC = max(natural, equipped + dex + magic + shield) per 5e.
|
|
17
|
+
|
|
18
|
+
|
|
3
19
|
All notable changes to dndwright are documented here. The format follows
|
|
4
20
|
[Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
|
|
5
21
|
[Semantic Versioning](https://semver.org/).
|
|
@@ -10,6 +26,50 @@ breaking changes; these will always be noted here.
|
|
|
10
26
|
|
|
11
27
|
## [Unreleased]
|
|
12
28
|
|
|
29
|
+
## [0.25.0] — 2026-06-13
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **Power-budget validator.** `validate_power_budget(species_data, class_data,
|
|
33
|
+
subclass_data, level)` returns budget overage problems (species traits,
|
|
34
|
+
class+subclass features per level, combined species-vs-learned split). Baselines
|
|
35
|
+
derived from dndwright's own SRD 5.2.1 content: 9 species (5 trait max, 3
|
|
36
|
+
high-impact max) and 12 classes (cumulative feature budgets by level). High-impact
|
|
37
|
+
traits (flight, innate casting, resistance, breath weapon) weighted heavier.
|
|
38
|
+
Superman+Batman double-stacking caught at low levels. Exported in `dndwright.__all__`.
|
|
39
|
+
- 17 new unit tests for power-budget coverage (balanced vs over-budget vs edge cases).
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- `__all__` expanded: +1 name (`validate_power_budget`).
|
|
43
|
+
|
|
44
|
+
## [0.24.1] — 2026-06-13
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
- Homebrew validators added to README feature table.
|
|
48
|
+
|
|
49
|
+
## [0.24.0] — 2026-06-13
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **Homebrew validation module.** New `rules/homebrew_validator.py` with
|
|
53
|
+
`validate_class_homebrew()`, `validate_species_homebrew()`,
|
|
54
|
+
`validate_subclass_homebrew()`, and `validate_background_homebrew()` — each
|
|
55
|
+
returns a list of human-readable rule-violation strings (empty = legal).
|
|
56
|
+
Structural checks per SRD 5.2.1: hit-die legality (d6/d8/d10/d12),
|
|
57
|
+
saving-throw proficiency pairs (one strong + one weak), archetype and
|
|
58
|
+
spellcasting-type validity, feature-level ranges (1–20, no duplicates),
|
|
59
|
+
species speed limits, background skill counts, and more.
|
|
60
|
+
- `validate_homebrew(component_type, data)` router and `HOMEBREW_VALIDATORS`
|
|
61
|
+
lookup dict. All new names exported in `dndwright.__all__`.
|
|
62
|
+
- 30+ unit tests for all validators, covering legal components (empty
|
|
63
|
+
return), illegal components (each check catches), and the aggregate
|
|
64
|
+
router.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- `__all__` expanded: +7 names (`validate_class_homebrew`,
|
|
68
|
+
`validate_species_homebrew`, `validate_subclass_homebrew`,
|
|
69
|
+
`validate_background_homebrew`, `validate_homebrew`, `HOMEBREW_VALIDATORS`).
|
|
70
|
+
- `tests/test_api_contract.py` `EXPECTED_PUBLIC` and `EXPECTED_SIGNATURES`
|
|
71
|
+
updated for the new exports.
|
|
72
|
+
|
|
13
73
|
## [0.23.2] — 2026-06-06
|
|
14
74
|
|
|
15
75
|
### Documentation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dndwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: Domain-neutral D&D 5e (2024) rules & character-sheet computation engine: a data-driven DAG of formulas (ability mods, proficiency, spell DC/slots, HP, AC).
|
|
5
5
|
Project-URL: Homepage, https://github.com/sligara7/dndwright
|
|
6
6
|
Project-URL: Repository, https://github.com/sligara7/dndwright
|
|
@@ -207,6 +207,7 @@ rs.lookup_tables["armor_base_ac"]["plate"] # 19 (base is still 18, untouched)
|
|
|
207
207
|
| `evaluate` / `assemble_character_inputs` / `apply_modifiers` | The lower-level engine. |
|
|
208
208
|
| `Ruleset` / `ComputationNode` / `FormulaSpec` / `NodeType` | The DAG schema. |
|
|
209
209
|
| `validate_ruleset` / `assert_valid_ruleset` | Static integrity check for a ruleset (unknown ops, cycles, dangling refs) — catch authoring errors before evaluation. |
|
|
210
|
+
| `validate_class_homebrew` / `validate_species_homebrew` / `validate_subclass_homebrew` / `validate_background_homebrew` / `validate_homebrew` | Validate homebrew class/species/subclass/background data against SRD 5.2.1 structural rules (hit die, save profs, archetype, feature levels, skill counts, speed limits). Returns `list[str]` of violations — empty = legal. |
|
|
210
211
|
| `compose` / `modifier` / `Component` | Snap mini-graphs (items/feats/traits) onto a ruleset; downstream values cascade. |
|
|
211
212
|
| `component_from_content` | Build a `Component` from a bundled item/feat's `component` field — magic items & feats as data that snap onto a character (constant, dynamic, player-chosen, or *conditional* effects). |
|
|
212
213
|
| `apply_theme_scaling` / `ThemeScalingLayer` / `get_theme_scaling` | Re-skin the ruleset for any setting (sci-fi, modern, steampunk, …): override node defaults & lookup tables and re-flavor names, same graph shape. `PREDEFINED_THEME_SCALING` ships ready-made themes. |
|
|
@@ -174,6 +174,7 @@ rs.lookup_tables["armor_base_ac"]["plate"] # 19 (base is still 18, untouched)
|
|
|
174
174
|
| `evaluate` / `assemble_character_inputs` / `apply_modifiers` | The lower-level engine. |
|
|
175
175
|
| `Ruleset` / `ComputationNode` / `FormulaSpec` / `NodeType` | The DAG schema. |
|
|
176
176
|
| `validate_ruleset` / `assert_valid_ruleset` | Static integrity check for a ruleset (unknown ops, cycles, dangling refs) — catch authoring errors before evaluation. |
|
|
177
|
+
| `validate_class_homebrew` / `validate_species_homebrew` / `validate_subclass_homebrew` / `validate_background_homebrew` / `validate_homebrew` | Validate homebrew class/species/subclass/background data against SRD 5.2.1 structural rules (hit die, save profs, archetype, feature levels, skill counts, speed limits). Returns `list[str]` of violations — empty = legal. |
|
|
177
178
|
| `compose` / `modifier` / `Component` | Snap mini-graphs (items/feats/traits) onto a ruleset; downstream values cascade. |
|
|
178
179
|
| `component_from_content` | Build a `Component` from a bundled item/feat's `component` field — magic items & feats as data that snap onto a character (constant, dynamic, player-chosen, or *conditional* effects). |
|
|
179
180
|
| `apply_theme_scaling` / `ThemeScalingLayer` / `get_theme_scaling` | Re-skin the ruleset for any setting (sci-fi, modern, steampunk, …): override node defaults & lookup tables and re-flavor names, same graph shape. `PREDEFINED_THEME_SCALING` ships ready-made themes. |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dndwright"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.26.0"
|
|
8
8
|
description = "Domain-neutral D&D 5e (2024) rules & character-sheet computation engine: a data-driven DAG of formulas (ability mods, proficiency, spell DC/slots, HP, AC)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -100,7 +100,18 @@ from .rules.validation import (
|
|
|
100
100
|
validate_ruleset,
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
-
__version__ = "0.
|
|
103
|
+
__version__ = "0.26.0"
|
|
104
|
+
|
|
105
|
+
# homebrew validation (structural rules checks on LLM-generated components)
|
|
106
|
+
from .rules.homebrew_validator import (
|
|
107
|
+
validate_class_homebrew,
|
|
108
|
+
validate_species_homebrew,
|
|
109
|
+
validate_subclass_homebrew,
|
|
110
|
+
validate_background_homebrew,
|
|
111
|
+
validate_homebrew,
|
|
112
|
+
validate_power_budget,
|
|
113
|
+
VALIDATORS as HOMEBREW_VALIDATORS,
|
|
114
|
+
)
|
|
104
115
|
|
|
105
116
|
__all__ = [
|
|
106
117
|
# high-level (dict in -> computed sheet out)
|
|
@@ -187,4 +198,12 @@ __all__ = [
|
|
|
187
198
|
"Modifier",
|
|
188
199
|
# dice engine (full typed surface in dndwright.dice)
|
|
189
200
|
"DiceEngine",
|
|
201
|
+
# homebrew validation (structural rules checks on LLM-generated components)
|
|
202
|
+
"validate_class_homebrew",
|
|
203
|
+
"validate_species_homebrew",
|
|
204
|
+
"validate_subclass_homebrew",
|
|
205
|
+
"validate_background_homebrew",
|
|
206
|
+
"validate_homebrew",
|
|
207
|
+
"validate_power_budget",
|
|
208
|
+
"HOMEBREW_VALIDATORS",
|
|
190
209
|
]
|
|
@@ -18,6 +18,66 @@ from .lookup_tables import (
|
|
|
18
18
|
SPELLCASTING_TYPE_BY_CLASS,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_hit_die(class_data: dict) -> int:
|
|
27
|
+
"""Return the resolved hit die size, applying lookup-table overrides.
|
|
28
|
+
|
|
29
|
+
Mirrors the logic in ``character_data_to_inputs`` lines 51-58 so the display
|
|
30
|
+
value matches the computation.
|
|
31
|
+
"""
|
|
32
|
+
hit_die_str = class_data.get("hit_die", "d8")
|
|
33
|
+
hit_die_size = int(hit_die_str.replace("d", "")) if isinstance(hit_die_str, str) else 8
|
|
34
|
+
|
|
35
|
+
class_name = class_data.get("class_name", "").lower()
|
|
36
|
+
archetype = (
|
|
37
|
+
class_data.get("archetype")
|
|
38
|
+
or class_data.get("properties", {}).get("archetype", "")
|
|
39
|
+
)
|
|
40
|
+
if class_name in HIT_DIE_BY_CLASS:
|
|
41
|
+
hit_die_size = HIT_DIE_BY_CLASS[class_name]
|
|
42
|
+
elif archetype in HIT_DIE_BY_ARCHETYPE:
|
|
43
|
+
hit_die_size = HIT_DIE_BY_ARCHETYPE[archetype]
|
|
44
|
+
|
|
45
|
+
return hit_die_size
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_NATURAL_ARMOR_KEYWORDS = re.compile(
|
|
49
|
+
r"(carapace|natural armor|chitin|exoskeleton|scales|tough hide|bark skin|"
|
|
50
|
+
r"thick fur|bony plates|armored shell)",
|
|
51
|
+
re.IGNORECASE,
|
|
52
|
+
)
|
|
53
|
+
_AC_VALUE_RE = re.compile(r"\bAC\b.*?(\d+)", re.IGNORECASE)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_natural_armor(species_data: dict) -> int:
|
|
57
|
+
"""Extract natural armor AC from species traits, or 0 if none."""
|
|
58
|
+
# Explicit field (future-proof)
|
|
59
|
+
explicit = species_data.get("natural_armor_ac")
|
|
60
|
+
if isinstance(explicit, int) and explicit > 0:
|
|
61
|
+
return explicit
|
|
62
|
+
|
|
63
|
+
traits: list[dict] = species_data.get("traits", []) or []
|
|
64
|
+
if not isinstance(traits, list):
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
for trait in traits:
|
|
68
|
+
if not isinstance(trait, dict):
|
|
69
|
+
continue
|
|
70
|
+
name = (trait.get("name") or "").lower()
|
|
71
|
+
desc = (trait.get("description") or "").lower()
|
|
72
|
+
combined = name + " " + desc
|
|
73
|
+
|
|
74
|
+
if _NATURAL_ARMOR_KEYWORDS.search(combined):
|
|
75
|
+
match = _AC_VALUE_RE.search(combined)
|
|
76
|
+
if match:
|
|
77
|
+
return int(match.group(1))
|
|
78
|
+
|
|
79
|
+
return 0
|
|
80
|
+
|
|
21
81
|
|
|
22
82
|
def character_data_to_inputs(
|
|
23
83
|
ability_scores: dict[str, int],
|
|
@@ -86,6 +146,9 @@ def character_data_to_inputs(
|
|
|
86
146
|
inputs["armor_magic_bonus"] = armor_magic
|
|
87
147
|
inputs["has_shield"] = has_shield
|
|
88
148
|
|
|
149
|
+
# --- Species natural armor ---
|
|
150
|
+
inputs["natural_armor_ac"] = _parse_natural_armor(species_data)
|
|
151
|
+
|
|
89
152
|
# --- Spellcasting ---
|
|
90
153
|
spellcasting_type = class_data.get("properties", {}).get("spellcasting_type") or class_data.get(
|
|
91
154
|
"spellcasting_type", "none"
|
|
@@ -227,7 +290,8 @@ def computed_values_to_sheet(
|
|
|
227
290
|
speed_val = speed
|
|
228
291
|
|
|
229
292
|
# --- Hit dice ---
|
|
230
|
-
|
|
293
|
+
hit_die_size = _resolve_hit_die(class_data)
|
|
294
|
+
hit_die = f"d{hit_die_size}"
|
|
231
295
|
hit_dice_str = computed.get("hit_dice", f"{level}{hit_die}")
|
|
232
296
|
|
|
233
297
|
# --- Spellcasting ---
|
|
@@ -153,6 +153,7 @@ def assemble_character_inputs(
|
|
|
153
153
|
inputs["armor_type"] = "none"
|
|
154
154
|
inputs["armor_magic_bonus"] = 0
|
|
155
155
|
inputs["has_shield"] = has_shield
|
|
156
|
+
inputs["natural_armor_ac"] = 0
|
|
156
157
|
|
|
157
158
|
# ------------------------------------------------------------------
|
|
158
159
|
# 8. Saving throw proficiencies
|
|
@@ -17,6 +17,7 @@ from typing import Any
|
|
|
17
17
|
|
|
18
18
|
from .adapters import character_data_to_inputs, computed_values_to_sheet
|
|
19
19
|
from .assembler import apply_modifiers
|
|
20
|
+
from .compose import Component, compose, component_from_dict
|
|
20
21
|
from .dnd_5e_2024 import DND_5E_2024_RULESET
|
|
21
22
|
from .evaluator import evaluate
|
|
22
23
|
from .theme_scaling import ThemeScalingLayer, apply_theme_scaling
|
|
@@ -178,6 +179,7 @@ def evaluate_character(
|
|
|
178
179
|
*,
|
|
179
180
|
strict: bool = False,
|
|
180
181
|
scaling: ThemeScalingLayer | None = None,
|
|
182
|
+
components: list | None = None,
|
|
181
183
|
) -> dict:
|
|
182
184
|
"""Evaluate a character from session data → full computed character sheet.
|
|
183
185
|
|
|
@@ -218,8 +220,15 @@ def evaluate_character(
|
|
|
218
220
|
equipment=fields["equipment"],
|
|
219
221
|
)
|
|
220
222
|
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
+
# Compose any adopted item/equipment components onto the ruleset before evaluating —
|
|
224
|
+
# e.g. an allocated "+1 armor_class" or "resistance to fire" item snaps its modifier onto
|
|
225
|
+
# the computed sheet (each `component` is a Component, or a component_from_dict-shaped spec
|
|
226
|
+
# dict as persisted on a GraphComponent). Then evaluate the (theme-scaled) computation graph.
|
|
227
|
+
ruleset = _ruleset_for(scaling)
|
|
228
|
+
if components:
|
|
229
|
+
comps = [c if isinstance(c, Component) else component_from_dict(c) for c in components]
|
|
230
|
+
ruleset = compose(ruleset, *comps)
|
|
231
|
+
computed = evaluate(ruleset, inputs)
|
|
223
232
|
|
|
224
233
|
# Apply NodeModifiers from feats, class features, etc.
|
|
225
234
|
computed = apply_modifiers(computed, inputs)
|
|
@@ -182,6 +182,20 @@ def _build_nodes() -> dict[str, ComputationNode]:
|
|
|
182
182
|
)
|
|
183
183
|
)
|
|
184
184
|
|
|
185
|
+
add(
|
|
186
|
+
ComputationNode(
|
|
187
|
+
id="natural_armor_ac",
|
|
188
|
+
node_type=NodeType.INPUT,
|
|
189
|
+
label="Natural Armor AC",
|
|
190
|
+
layer=0,
|
|
191
|
+
group="equipment",
|
|
192
|
+
default_value=0,
|
|
193
|
+
min_value=0,
|
|
194
|
+
max_value=30,
|
|
195
|
+
description="Base AC from species natural armor (0 = none).",
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
185
199
|
add(
|
|
186
200
|
ComputationNode(
|
|
187
201
|
id="speed_base",
|
|
@@ -446,9 +460,9 @@ def _build_nodes() -> dict[str, ComputationNode]:
|
|
|
446
460
|
group="combat",
|
|
447
461
|
formula=FormulaSpec(
|
|
448
462
|
op="ac_with_armor",
|
|
449
|
-
args=["armor_type", "dexterity_mod", "armor_magic_bonus", "has_shield"],
|
|
463
|
+
args=["armor_type", "dexterity_mod", "armor_magic_bonus", "has_shield", "natural_armor_ac"],
|
|
450
464
|
),
|
|
451
|
-
inputs=["armor_type", "dexterity_mod", "armor_magic_bonus", "has_shield"],
|
|
465
|
+
inputs=["armor_type", "dexterity_mod", "armor_magic_bonus", "has_shield", "natural_armor_ac"],
|
|
452
466
|
min_value=1,
|
|
453
467
|
description="Base AC from armor + DEX (capped) + magic + shield. PHB p.14.",
|
|
454
468
|
)
|