dndwright 0.2.0__py3-none-any.whl
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/__init__.py +69 -0
- dndwright/content/__init__.py +56 -0
- dndwright/content/classes.json +147 -0
- dndwright/content/creatures.json +334 -0
- dndwright/content/generate.py +87 -0
- dndwright/content/magic_items.json +1915 -0
- dndwright/content/species.json +111 -0
- dndwright/ontology/__init__.py +25 -0
- dndwright/ontology/dnd.yaml +141 -0
- dndwright/ontology/loader.py +104 -0
- dndwright/rules/__init__.py +35 -0
- dndwright/rules/adapters.py +691 -0
- dndwright/rules/assembler.py +377 -0
- dndwright/rules/character_evaluator.py +248 -0
- dndwright/rules/components.py +654 -0
- dndwright/rules/dnd_5e_2024.py +606 -0
- dndwright/rules/evaluator.py +217 -0
- dndwright/rules/lookup_tables.py +501 -0
- dndwright/rules/operations.py +412 -0
- dndwright/rules/schema.py +69 -0
- dndwright/rules/theme_scaling.py +207 -0
- dndwright-0.2.0.dist-info/METADATA +101 -0
- dndwright-0.2.0.dist-info/RECORD +26 -0
- dndwright-0.2.0.dist-info/WHEEL +4 -0
- dndwright-0.2.0.dist-info/licenses/LICENSE +21 -0
- dndwright-0.2.0.dist-info/licenses/NOTICE +24 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"source": "dndwright original homebrew",
|
|
4
|
+
"license": "Original content — not derived from any copyrighted RPG product.",
|
|
5
|
+
"note": "Sample starter content; generate more with dndwright.content.generate."
|
|
6
|
+
},
|
|
7
|
+
"species": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Kryth-Vaal",
|
|
10
|
+
"mechanics": {
|
|
11
|
+
"size": "Medium",
|
|
12
|
+
"speed": 30,
|
|
13
|
+
"ability_bonuses": "+2 Strength, +1 Constitution",
|
|
14
|
+
"traits": [
|
|
15
|
+
"Crystalline carapace grants natural armor",
|
|
16
|
+
"Resonant hum creates minor sonic pulses",
|
|
17
|
+
"Photosynthetic skin heals in sunlight"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"narrative": {
|
|
21
|
+
"description": "Living statues composed of sentient quartz and volcanic glass. They process solar energy into kinetic force.",
|
|
22
|
+
"flavor": "Born from the heat of the mantle, tempered by the light of the stars."
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "Orynthian",
|
|
27
|
+
"mechanics": {
|
|
28
|
+
"size": "Small",
|
|
29
|
+
"speed": 30,
|
|
30
|
+
"ability_bonuses": "+2 Dexterity, +1 Intelligence",
|
|
31
|
+
"traits": [
|
|
32
|
+
"Gliding membranes under outstretched arms",
|
|
33
|
+
"Multi-faceted eyes grant darkvision",
|
|
34
|
+
"Prehensile quill-tipped tail"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"narrative": {
|
|
38
|
+
"description": "Avian-insectoid hybrids with slender frames and iridescent, hive-integrated consciousness. They excel at high-altitude navigation.",
|
|
39
|
+
"flavor": "The wind is not merely a path, but the language they speak."
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Mire-Strider",
|
|
44
|
+
"mechanics": {
|
|
45
|
+
"size": "Medium",
|
|
46
|
+
"speed": 30,
|
|
47
|
+
"ability_bonuses": "flexible +2/+1",
|
|
48
|
+
"traits": [
|
|
49
|
+
"Amphibious dual-lung system",
|
|
50
|
+
"Mud-camo grants stealth in foliage",
|
|
51
|
+
"Enzymatic touch dissolves organic bonds"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"narrative": {
|
|
55
|
+
"description": "Slime-skinned beings with symbiotic fungal colonies living within their porous outer layers. They act as the immune system of the wetlands.",
|
|
56
|
+
"flavor": "Where the water meets the rot, the Strider finds home."
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "Nebulite",
|
|
61
|
+
"mechanics": {
|
|
62
|
+
"size": "Medium",
|
|
63
|
+
"speed": 30,
|
|
64
|
+
"ability_bonuses": "+2 Charisma, +1 Wisdom",
|
|
65
|
+
"traits": [
|
|
66
|
+
"Incorporeal shimmer avoids opportunity attacks",
|
|
67
|
+
"Gravitational tether lifts small objects",
|
|
68
|
+
"Stellar glow illuminates dark spaces"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"narrative": {
|
|
72
|
+
"description": "Sentient gas clouds contained within intricate, self-forged brass shells. They are chroniclers of cosmic anomalies.",
|
|
73
|
+
"flavor": "A flicker of starlight trapped in a cage of clockwork."
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "Thrum-Kin",
|
|
78
|
+
"mechanics": {
|
|
79
|
+
"size": "Small",
|
|
80
|
+
"speed": 30,
|
|
81
|
+
"ability_bonuses": "+2 Constitution, +1 Charisma",
|
|
82
|
+
"traits": [
|
|
83
|
+
"Vibration sense detects movement",
|
|
84
|
+
"Sonic scream creates minor concussions",
|
|
85
|
+
"Dense fur absorbs elemental impact"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"narrative": {
|
|
89
|
+
"description": "Subterranean mammals that communicate through rhythmic drumming against cave walls. They possess deep empathy for the tremors of the earth.",
|
|
90
|
+
"flavor": "They hear the heartbeat of the world before it even begins to pulse."
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "Xylosian",
|
|
95
|
+
"mechanics": {
|
|
96
|
+
"size": "Medium",
|
|
97
|
+
"speed": 30,
|
|
98
|
+
"ability_bonuses": "+2 Intelligence, +1 Wisdom",
|
|
99
|
+
"traits": [
|
|
100
|
+
"Root-link telepathy with other Xylosians",
|
|
101
|
+
"Hardwood skin hardens when stationary",
|
|
102
|
+
"Chlorophyll stores allow for brief starvation"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"narrative": {
|
|
106
|
+
"description": "Bipedal plant-folk whose blood is essentially concentrated sap. They spend decades in deep meditation to gain ancestral knowledge.",
|
|
107
|
+
"flavor": "Growth is slow, but the memory of the forest is eternal."
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""D&D component ontology — a graph schema for D&D building blocks.
|
|
2
|
+
|
|
3
|
+
from dndwright import load_ontology
|
|
4
|
+
onto = load_ontology()
|
|
5
|
+
onto.node_types["Class"].properties["hit_die"].type # "string"
|
|
6
|
+
onto.edges_from("Character") # ["HAS_CLASS", ...]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .loader import (
|
|
10
|
+
EdgeTypeDef,
|
|
11
|
+
NodeTypeDef,
|
|
12
|
+
Ontology,
|
|
13
|
+
PropertyDef,
|
|
14
|
+
load_ontology,
|
|
15
|
+
parse_ontology,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"load_ontology",
|
|
20
|
+
"parse_ontology",
|
|
21
|
+
"Ontology",
|
|
22
|
+
"NodeTypeDef",
|
|
23
|
+
"EdgeTypeDef",
|
|
24
|
+
"PropertyDef",
|
|
25
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# dndwright — D&D 5e (2024) component ontology
|
|
2
|
+
#
|
|
3
|
+
# A graph schema for the building blocks of a D&D character/world: the mechanical
|
|
4
|
+
# component types (Class, Species, Spell, …) and how a Character connects to them.
|
|
5
|
+
# Domain-neutral — a host app supplies the `Character` (and any narrative) nodes;
|
|
6
|
+
# this schema describes the D&D components those nodes attach to.
|
|
7
|
+
#
|
|
8
|
+
# `mechanics` / `narrative` properties hold a JSON blob (rules data / flavour),
|
|
9
|
+
# so a component can carry arbitrary structured detail without schema churn.
|
|
10
|
+
#
|
|
11
|
+
# Component facts derive from the D&D SRD 5.2 (CC-BY-4.0); see NOTICE.
|
|
12
|
+
|
|
13
|
+
schema:
|
|
14
|
+
name: dnd
|
|
15
|
+
version: 1
|
|
16
|
+
|
|
17
|
+
node_types:
|
|
18
|
+
|
|
19
|
+
Class:
|
|
20
|
+
properties:
|
|
21
|
+
name: { type: string, required: true }
|
|
22
|
+
description: { type: string }
|
|
23
|
+
hit_die: { type: string, description: "Hit die, e.g. 'd8' (string so the 'd' prefix shows)." }
|
|
24
|
+
primary_ability: { type: string }
|
|
25
|
+
mechanics: { type: string, description: "JSON: progression, proficiencies, spellcasting, features." }
|
|
26
|
+
narrative: { type: string, description: "JSON: concept, role, flavour." }
|
|
27
|
+
|
|
28
|
+
Subclass:
|
|
29
|
+
properties:
|
|
30
|
+
name: { type: string, required: true }
|
|
31
|
+
class_name: { type: string, description: "Name of the parent Class." }
|
|
32
|
+
description: { type: string }
|
|
33
|
+
mechanics: { type: string }
|
|
34
|
+
narrative: { type: string }
|
|
35
|
+
|
|
36
|
+
Species:
|
|
37
|
+
properties:
|
|
38
|
+
name: { type: string, required: true }
|
|
39
|
+
description: { type: string }
|
|
40
|
+
size: { type: string }
|
|
41
|
+
speed: { type: int }
|
|
42
|
+
mechanics: { type: string, description: "JSON: traits, ability bonuses." }
|
|
43
|
+
narrative: { type: string }
|
|
44
|
+
|
|
45
|
+
Background:
|
|
46
|
+
properties:
|
|
47
|
+
name: { type: string, required: true }
|
|
48
|
+
description: { type: string }
|
|
49
|
+
mechanics: { type: string }
|
|
50
|
+
narrative: { type: string }
|
|
51
|
+
|
|
52
|
+
Feat:
|
|
53
|
+
properties:
|
|
54
|
+
name: { type: string, required: true }
|
|
55
|
+
description: { type: string }
|
|
56
|
+
prerequisite: { type: string }
|
|
57
|
+
mechanics: { type: string }
|
|
58
|
+
|
|
59
|
+
Spell:
|
|
60
|
+
properties:
|
|
61
|
+
name: { type: string, required: true }
|
|
62
|
+
level: { type: int }
|
|
63
|
+
school: { type: string }
|
|
64
|
+
description: { type: string }
|
|
65
|
+
mechanics: { type: string }
|
|
66
|
+
narrative: { type: string }
|
|
67
|
+
|
|
68
|
+
Equipment:
|
|
69
|
+
properties:
|
|
70
|
+
name: { type: string, required: true }
|
|
71
|
+
equipment_type: { type: string, description: "weapon | armor | gear | tool | …" }
|
|
72
|
+
rarity: { type: string }
|
|
73
|
+
description: { type: string }
|
|
74
|
+
mechanics: { type: string, description: "JSON: damage, properties, cost, weight." }
|
|
75
|
+
narrative: { type: string }
|
|
76
|
+
|
|
77
|
+
MagicItem:
|
|
78
|
+
properties:
|
|
79
|
+
name: { type: string, required: true }
|
|
80
|
+
category: { type: string, indexed: true, description: "Armor | Potion | Ring | Rod | Scroll | Staff | Wand | Weapon | Wondrous Item." }
|
|
81
|
+
rarity: { type: string, indexed: true, description: "common | uncommon | rare | very_rare | legendary | artifact." }
|
|
82
|
+
attunement_required: { type: bool, indexed: true }
|
|
83
|
+
type_line: { type: string, description: "Display type line, e.g. 'Wondrous Item, Rare (Requires Attunement)'." }
|
|
84
|
+
description: { type: string }
|
|
85
|
+
# True for reusable library templates; instances copy from them (see INSTANCE_OF).
|
|
86
|
+
is_library: { type: bool, indexed: true }
|
|
87
|
+
|
|
88
|
+
Creature:
|
|
89
|
+
properties:
|
|
90
|
+
name: { type: string, required: true }
|
|
91
|
+
creature_type: { type: string, description: "aberration | beast | dragon | fiend | undead | …" }
|
|
92
|
+
size: { type: string }
|
|
93
|
+
alignment: { type: string }
|
|
94
|
+
cr: { type: string, description: "Challenge rating as a string, e.g. '1/4', '8'." }
|
|
95
|
+
challenge_rating: { type: float, description: "Numeric CR for sorting/budgeting (1/4 -> 0.25)." }
|
|
96
|
+
ac: { type: int }
|
|
97
|
+
hp_max: { type: int }
|
|
98
|
+
speed_walk: { type: int }
|
|
99
|
+
# Full stat block as a JSON string (abilities, actions, traits, …).
|
|
100
|
+
stat_block: { type: string }
|
|
101
|
+
description: { type: string }
|
|
102
|
+
is_library: { type: bool, indexed: true }
|
|
103
|
+
embedding_field: description
|
|
104
|
+
|
|
105
|
+
edge_types:
|
|
106
|
+
|
|
107
|
+
# === A Character is built from components ===
|
|
108
|
+
HAS_CLASS: { from: Character, to: Class }
|
|
109
|
+
HAS_SUBCLASS: { from: Character, to: Subclass }
|
|
110
|
+
HAS_SPECIES: { from: Character, to: Species }
|
|
111
|
+
HAS_BACKGROUND: { from: Character, to: Background }
|
|
112
|
+
HAS_FEAT: { from: Character, to: Feat }
|
|
113
|
+
HAS_SPELL: { from: Character, to: Spell }
|
|
114
|
+
HAS_EQUIPMENT: { from: Character, to: Equipment }
|
|
115
|
+
|
|
116
|
+
SUBCLASS_OF:
|
|
117
|
+
from: [Subclass]
|
|
118
|
+
to: [Class]
|
|
119
|
+
description: "A Subclass specialises a Class."
|
|
120
|
+
|
|
121
|
+
# === Mechanical form of an entity ===
|
|
122
|
+
HAS_STAT_BLOCK:
|
|
123
|
+
from: [Character]
|
|
124
|
+
to: [MagicItem, Equipment, Creature]
|
|
125
|
+
properties:
|
|
126
|
+
is_overridden: { type: bool, description: "Instance values diverge from its INSTANCE_OF template." }
|
|
127
|
+
description: >
|
|
128
|
+
Bridge from an identity-bearing entity to its D&D mechanical form
|
|
129
|
+
(e.g. a named NPC -> a Creature stat block; a signature weapon -> a
|
|
130
|
+
MagicItem/Equipment). A host app may extend `from` with its own
|
|
131
|
+
narrative node types.
|
|
132
|
+
|
|
133
|
+
# === Library template / instance ===
|
|
134
|
+
INSTANCE_OF:
|
|
135
|
+
from: [MagicItem, Equipment, Creature]
|
|
136
|
+
to: [MagicItem, Equipment, Creature]
|
|
137
|
+
description: >
|
|
138
|
+
Links a campaign-scoped instance to a reusable library template
|
|
139
|
+
(target is_library=true, source is_library=false), same type only.
|
|
140
|
+
Instances copy template properties at creation; this edge is for
|
|
141
|
+
traceability / 'modified-from-template' diffing, not runtime fallback.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Load and model the D&D component ontology (``dnd.yaml``).
|
|
2
|
+
|
|
3
|
+
Parses the bundled graph schema into typed, queryable pydantic models: node types
|
|
4
|
+
(with typed properties) and edge types (with normalised from/to endpoints). A host
|
|
5
|
+
graph app can use this to validate or drive its own D&D component graph.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.resources
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PropertyDef(BaseModel):
|
|
18
|
+
"""A node/edge property definition."""
|
|
19
|
+
|
|
20
|
+
type: str
|
|
21
|
+
required: bool = False
|
|
22
|
+
indexed: bool = False
|
|
23
|
+
description: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NodeTypeDef(BaseModel):
|
|
27
|
+
"""A node type: a named set of typed properties."""
|
|
28
|
+
|
|
29
|
+
properties: dict[str, PropertyDef] = Field(default_factory=dict)
|
|
30
|
+
embedding_field: str | None = None
|
|
31
|
+
|
|
32
|
+
def required_properties(self) -> list[str]:
|
|
33
|
+
return [name for name, p in self.properties.items() if p.required]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EdgeTypeDef(BaseModel):
|
|
37
|
+
"""An edge type with normalised ``source``/``target`` node-type lists."""
|
|
38
|
+
|
|
39
|
+
source: list[str] = Field(default_factory=list) # the schema's `from`
|
|
40
|
+
target: list[str] = Field(default_factory=list) # the schema's `to`
|
|
41
|
+
properties: dict[str, PropertyDef] = Field(default_factory=dict)
|
|
42
|
+
description: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Ontology(BaseModel):
|
|
46
|
+
"""The parsed component ontology: node types + edge types."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
version: int
|
|
50
|
+
node_types: dict[str, NodeTypeDef] = Field(default_factory=dict)
|
|
51
|
+
edge_types: dict[str, EdgeTypeDef] = Field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def edges_from(self, node_type: str) -> list[str]:
|
|
54
|
+
"""Edge-type names that may originate at ``node_type``."""
|
|
55
|
+
return [n for n, e in self.edge_types.items() if node_type in e.source]
|
|
56
|
+
|
|
57
|
+
def edges_to(self, node_type: str) -> list[str]:
|
|
58
|
+
"""Edge-type names that may point at ``node_type``."""
|
|
59
|
+
return [n for n, e in self.edge_types.items() if node_type in e.target]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _as_list(value: object) -> list[str]:
|
|
63
|
+
if value is None:
|
|
64
|
+
return []
|
|
65
|
+
return list(value) if isinstance(value, list) else [str(value)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_ontology(raw: dict) -> Ontology:
|
|
69
|
+
"""Build an :class:`Ontology` from a parsed schema mapping."""
|
|
70
|
+
schema = raw["schema"]
|
|
71
|
+
node_types = {
|
|
72
|
+
name: NodeTypeDef(**(nt or {}))
|
|
73
|
+
for name, nt in (schema.get("node_types") or {}).items()
|
|
74
|
+
}
|
|
75
|
+
edge_types: dict[str, EdgeTypeDef] = {}
|
|
76
|
+
for name, edge in (schema.get("edge_types") or {}).items():
|
|
77
|
+
edge = edge or {}
|
|
78
|
+
edge_types[name] = EdgeTypeDef(
|
|
79
|
+
source=_as_list(edge.get("from")),
|
|
80
|
+
target=_as_list(edge.get("to")),
|
|
81
|
+
properties={
|
|
82
|
+
p: PropertyDef(**(d or {})) for p, d in (edge.get("properties") or {}).items()
|
|
83
|
+
},
|
|
84
|
+
description=(str(edge.get("description")).strip() or None)
|
|
85
|
+
if edge.get("description")
|
|
86
|
+
else None,
|
|
87
|
+
)
|
|
88
|
+
return Ontology(
|
|
89
|
+
name=schema["name"],
|
|
90
|
+
version=schema["version"],
|
|
91
|
+
node_types=node_types,
|
|
92
|
+
edge_types=edge_types,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_ontology(path: str | Path | None = None) -> Ontology:
|
|
97
|
+
"""Load an ontology. With no ``path``, loads the bundled D&D ``dnd.yaml``."""
|
|
98
|
+
if path is None:
|
|
99
|
+
text = (importlib.resources.files("dndwright.ontology") / "dnd.yaml").read_text(
|
|
100
|
+
encoding="utf-8"
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
104
|
+
return parse_ontology(yaml.safe_load(text))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Character sheet computation graph engine.
|
|
2
|
+
|
|
3
|
+
Models D&D character sheets as a directed acyclic graph where nodes are values
|
|
4
|
+
and edges are dependencies. Formulas are data (JSON-serializable DSL), not code.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from rules.dnd_5e_2024 import DND_5E_2024_RULESET
|
|
8
|
+
from rules.evaluator import evaluate
|
|
9
|
+
from rules.assembler import assemble_character_inputs, apply_modifiers
|
|
10
|
+
from rules.components import ClassMechanics, SpeciesMechanics, ...
|
|
11
|
+
|
|
12
|
+
inputs = assemble_character_inputs(
|
|
13
|
+
class_mechanics=class_mech,
|
|
14
|
+
species_mechanics=species_mech,
|
|
15
|
+
background_mechanics=bg_mech,
|
|
16
|
+
ability_scores={...},
|
|
17
|
+
level=5,
|
|
18
|
+
)
|
|
19
|
+
computed = evaluate(DND_5E_2024_RULESET, inputs)
|
|
20
|
+
computed = apply_modifiers(computed, inputs)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .assembler import apply_modifiers, assemble_character_inputs
|
|
24
|
+
from .evaluator import evaluate
|
|
25
|
+
from .schema import ComputationNode, FormulaSpec, NodeType, Ruleset
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ComputationNode",
|
|
29
|
+
"FormulaSpec",
|
|
30
|
+
"NodeType",
|
|
31
|
+
"Ruleset",
|
|
32
|
+
"apply_modifiers",
|
|
33
|
+
"assemble_character_inputs",
|
|
34
|
+
"evaluate",
|
|
35
|
+
]
|