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,654 @@
|
|
|
1
|
+
"""Component mechanical and narrative schemas.
|
|
2
|
+
|
|
3
|
+
Each D&D character component (class, species, subclass, background) has:
|
|
4
|
+
- A Mechanics model: the independent variables the LLM must decide.
|
|
5
|
+
These feed directly into the computation graph as input overrides.
|
|
6
|
+
- A Narrative model: flavor, lore, descriptions.
|
|
7
|
+
These go to the knowledge graph (Neo4j), not the computation graph.
|
|
8
|
+
|
|
9
|
+
The LLM generates both, but they're cleanly separated. The computation
|
|
10
|
+
graph only ever sees Mechanics. The knowledge graph only ever sees Narrative.
|
|
11
|
+
|
|
12
|
+
Creature/monster stat blocks follow the same split.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Literal
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Shared types
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
AbilityName = Literal[
|
|
26
|
+
"strength", "dexterity", "constitution",
|
|
27
|
+
"intelligence", "wisdom", "charisma",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
SpellcastingType = Literal[
|
|
31
|
+
"none", "full_caster", "half_caster", "pact_caster",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
DieSize = Literal[6, 8, 10, 12]
|
|
35
|
+
|
|
36
|
+
CreatureSize = Literal["Tiny", "Small", "Medium", "Large", "Huge", "Gargantuan"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Resource Mechanic (class-specific resource like Ki, Rage, Sorcery Points)
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ResourceMechanic(BaseModel):
|
|
45
|
+
"""A class's unique resource pool. Mechanical definition only."""
|
|
46
|
+
|
|
47
|
+
starting_amount: int = Field(ge=0, description="Amount at level 1")
|
|
48
|
+
max_amount: int = Field(ge=0, description="Amount at level 20")
|
|
49
|
+
recovery: Literal["short_rest", "long_rest"] = "short_rest"
|
|
50
|
+
scaling: str = Field(
|
|
51
|
+
default="",
|
|
52
|
+
description="Brief rule for how it scales, e.g. 'equal to class level'",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ResourceNarrative(BaseModel):
|
|
57
|
+
"""Narrative flavor for a class resource."""
|
|
58
|
+
|
|
59
|
+
name: str = Field(description="Thematic name, e.g. 'Harmonics', 'Force Points'")
|
|
60
|
+
usage_description: str = Field(
|
|
61
|
+
description="What spending 1 point feels like narratively"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# CLASS
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ClassMechanics(BaseModel):
|
|
71
|
+
"""Independent mechanical values for a character class.
|
|
72
|
+
|
|
73
|
+
These are the ONLY values the LLM needs to decide for a class.
|
|
74
|
+
Everything else (HP, proficiency bonus, save bonuses, spell save DC)
|
|
75
|
+
is computed by the graph from these inputs.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
hit_die: DieSize
|
|
79
|
+
saving_throw_proficiencies: list[AbilityName] = Field(min_length=2, max_length=2)
|
|
80
|
+
armor_proficiencies: list[str] = Field(default_factory=list)
|
|
81
|
+
weapon_proficiencies: list[str] = Field(default_factory=list)
|
|
82
|
+
skill_proficiency_options: list[str] = Field(default_factory=list)
|
|
83
|
+
skill_proficiency_count: int = Field(default=2, ge=1, le=5)
|
|
84
|
+
spellcasting_type: SpellcastingType = "none"
|
|
85
|
+
spellcasting_ability: AbilityName | None = None
|
|
86
|
+
resource_mechanic: ResourceMechanic | None = None
|
|
87
|
+
archetype: str = Field(
|
|
88
|
+
description="full_martial, skill_martial, primal_martial, "
|
|
89
|
+
"half_caster, full_caster, pact_caster, support_caster"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Level-gated features (mechanical effects only)
|
|
93
|
+
progression: list[LevelFeatureMechanics] = Field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class LevelFeatureMechanics(BaseModel):
|
|
97
|
+
"""Mechanical effect of a class feature at a specific level.
|
|
98
|
+
|
|
99
|
+
Only tracks what mechanically changes. The name and flavor text
|
|
100
|
+
live in LevelFeatureNarrative.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
level: int = Field(ge=1, le=20)
|
|
104
|
+
feature_id: str = Field(description="Stable ID for this feature, e.g. 'extra_attack'")
|
|
105
|
+
grants_proficiency: list[str] = Field(default_factory=list)
|
|
106
|
+
grants_resistance: list[str] = Field(default_factory=list)
|
|
107
|
+
grants_expertise: list[str] = Field(default_factory=list)
|
|
108
|
+
modifies_node: list[NodeModifier] = Field(
|
|
109
|
+
default_factory=list,
|
|
110
|
+
description="Direct overrides to computation graph nodes",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class NodeModifier(BaseModel):
|
|
115
|
+
"""A modifier that a feature/item/feat applies to a graph node.
|
|
116
|
+
|
|
117
|
+
This is how mini-graphs plug into the main character graph.
|
|
118
|
+
Example: Alert feat → NodeModifier(target_node="initiative", op="add", value=5)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
target_node: str = Field(description="ID of the graph node to modify")
|
|
122
|
+
op: Literal["add", "set", "max", "min", "multiply"] = "add"
|
|
123
|
+
value: int | float | bool | str = Field(description="The modifier value")
|
|
124
|
+
condition: str | None = Field(
|
|
125
|
+
default=None,
|
|
126
|
+
description="Optional condition, e.g. 'level >= 5'",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ClassNarrative(BaseModel):
|
|
131
|
+
"""Narrative/flavor content for a character class."""
|
|
132
|
+
|
|
133
|
+
class_name: str
|
|
134
|
+
description: str = Field(description="1-2 paragraph class fantasy")
|
|
135
|
+
resource_narrative: ResourceNarrative | None = None
|
|
136
|
+
progression_flavor: list[LevelFeatureNarrative] = Field(default_factory=list)
|
|
137
|
+
signature_spell_lore: list[SpellNarrative] = Field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class LevelFeatureNarrative(BaseModel):
|
|
141
|
+
"""Name and flavor text for a class feature."""
|
|
142
|
+
|
|
143
|
+
level: int = Field(ge=1, le=20)
|
|
144
|
+
feature_id: str
|
|
145
|
+
name: str
|
|
146
|
+
description: str = Field(description="Narrative description of what this looks like")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class SpellNarrative(BaseModel):
|
|
150
|
+
"""Narrative content for a signature spell."""
|
|
151
|
+
|
|
152
|
+
spell_id: str = Field(description="Stable ID matching a spell mechanics entry")
|
|
153
|
+
themed_name: str
|
|
154
|
+
lore: str = Field(description="1-2 paragraphs connecting spell to class identity")
|
|
155
|
+
manifestation: str = Field(description="Visual description of casting")
|
|
156
|
+
symbolism: list[str] = Field(default_factory=list)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ClassGenerationOutput(BaseModel):
|
|
160
|
+
"""Complete LLM output for class generation — mechanics + narrative split."""
|
|
161
|
+
|
|
162
|
+
mechanics: ClassMechanics
|
|
163
|
+
narrative: ClassNarrative
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# =============================================================================
|
|
167
|
+
# SPECIES
|
|
168
|
+
# =============================================================================
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SpeciesMechanics(BaseModel):
|
|
172
|
+
"""Independent mechanical values for a character species."""
|
|
173
|
+
|
|
174
|
+
size: Literal["Small", "Medium"] = "Medium"
|
|
175
|
+
creature_type: str = "Humanoid"
|
|
176
|
+
speed: SpeciesSpeed = Field(default_factory=lambda: SpeciesSpeed())
|
|
177
|
+
darkvision: int | None = Field(default=None, description="Darkvision range in feet")
|
|
178
|
+
resistances: list[str] = Field(default_factory=list)
|
|
179
|
+
damage_immunities: list[str] = Field(default_factory=list)
|
|
180
|
+
condition_immunities: list[str] = Field(default_factory=list)
|
|
181
|
+
skill_proficiencies: list[str] = Field(default_factory=list)
|
|
182
|
+
tool_proficiencies: list[str] = Field(default_factory=list)
|
|
183
|
+
languages: list[str] = Field(default_factory=lambda: ["Common"])
|
|
184
|
+
natural_weapons: list[NaturalWeaponMechanics] = Field(default_factory=list)
|
|
185
|
+
traits: list[SpeciesTraitMechanics] = Field(default_factory=list)
|
|
186
|
+
innate_spellcasting: InnateSpellcasting | None = None
|
|
187
|
+
rest_type: Literal["standard", "trance", "sentry"] = "standard"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class SpeciesSpeed(BaseModel):
|
|
191
|
+
"""Movement speeds for a species."""
|
|
192
|
+
|
|
193
|
+
walk: int = 30
|
|
194
|
+
fly: int | None = None
|
|
195
|
+
swim: int | None = None
|
|
196
|
+
climb: int | None = None
|
|
197
|
+
burrow: int | None = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class NaturalWeaponMechanics(BaseModel):
|
|
201
|
+
"""Mechanical definition of a natural weapon."""
|
|
202
|
+
|
|
203
|
+
weapon_id: str
|
|
204
|
+
damage: str = Field(description="Damage dice, e.g. '1d6'")
|
|
205
|
+
damage_type: str = "slashing"
|
|
206
|
+
properties: list[str] = Field(default_factory=list)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class SpeciesTraitMechanics(BaseModel):
|
|
210
|
+
"""Mechanical effect of a species trait."""
|
|
211
|
+
|
|
212
|
+
trait_id: str = Field(description="Stable ID, e.g. 'relentless_endurance'")
|
|
213
|
+
action_type: Literal["passive", "action", "bonus_action", "reaction"] | None = None
|
|
214
|
+
modifies_node: list[NodeModifier] = Field(default_factory=list)
|
|
215
|
+
grants_proficiency: list[str] = Field(default_factory=list)
|
|
216
|
+
grants_resistance: list[str] = Field(default_factory=list)
|
|
217
|
+
uses_per_rest: int | None = Field(
|
|
218
|
+
default=None, description="Uses before needing a rest"
|
|
219
|
+
)
|
|
220
|
+
recovery: Literal["short_rest", "long_rest"] | None = None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class InnateSpellcasting(BaseModel):
|
|
224
|
+
"""Innate spellcasting from species (not class)."""
|
|
225
|
+
|
|
226
|
+
ability: AbilityName
|
|
227
|
+
spells: list[InnateSpellMechanics] = Field(default_factory=list)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class InnateSpellMechanics(BaseModel):
|
|
231
|
+
"""Mechanical definition of an innate spell."""
|
|
232
|
+
|
|
233
|
+
spell_id: str
|
|
234
|
+
base_spell: str = Field(description="Official D&D spell name or 'custom'")
|
|
235
|
+
level: int = Field(ge=0, le=9)
|
|
236
|
+
uses_per_day: int | None = Field(
|
|
237
|
+
default=None, description="None = at will (cantrips)"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class SpeciesNarrative(BaseModel):
|
|
242
|
+
"""Narrative/flavor content for a species."""
|
|
243
|
+
|
|
244
|
+
species_name: str
|
|
245
|
+
description: str = Field(description="1-2 paragraphs on origin and nature")
|
|
246
|
+
lifespan_description: str = ""
|
|
247
|
+
trait_flavor: list[SpeciesTraitNarrative] = Field(default_factory=list)
|
|
248
|
+
natural_weapon_flavor: list[NaturalWeaponNarrative] = Field(default_factory=list)
|
|
249
|
+
innate_spell_lore: list[SpellNarrative] = Field(default_factory=list)
|
|
250
|
+
lineages: list[LineageNarrative] = Field(default_factory=list)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class SpeciesTraitNarrative(BaseModel):
|
|
254
|
+
"""Name and flavor text for a species trait."""
|
|
255
|
+
|
|
256
|
+
trait_id: str
|
|
257
|
+
name: str
|
|
258
|
+
description: str
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class NaturalWeaponNarrative(BaseModel):
|
|
262
|
+
"""Flavor for a natural weapon."""
|
|
263
|
+
|
|
264
|
+
weapon_id: str
|
|
265
|
+
name: str
|
|
266
|
+
description: str
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class LineageNarrative(BaseModel):
|
|
270
|
+
"""A species sub-lineage with narrative features."""
|
|
271
|
+
|
|
272
|
+
name: str
|
|
273
|
+
description: str
|
|
274
|
+
features: list[str] = Field(default_factory=list)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class SpeciesGenerationOutput(BaseModel):
|
|
278
|
+
"""Complete LLM output for species generation."""
|
|
279
|
+
|
|
280
|
+
mechanics: SpeciesMechanics
|
|
281
|
+
narrative: SpeciesNarrative
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# =============================================================================
|
|
285
|
+
# SUBCLASS
|
|
286
|
+
# =============================================================================
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class SubclassMechanics(BaseModel):
|
|
290
|
+
"""Independent mechanical values for a subclass."""
|
|
291
|
+
|
|
292
|
+
subclass_archetype: str = Field(
|
|
293
|
+
description="damage_dealer, tank_protector, support, control, healer"
|
|
294
|
+
)
|
|
295
|
+
bonus_proficiencies: list[str] = Field(default_factory=list)
|
|
296
|
+
domain_spells: list[DomainSpellLevel] = Field(default_factory=list)
|
|
297
|
+
features: list[LevelFeatureMechanics] = Field(default_factory=list)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class DomainSpellLevel(BaseModel):
|
|
301
|
+
"""Spells granted at a specific spell level."""
|
|
302
|
+
|
|
303
|
+
spell_level: int = Field(ge=1, le=9)
|
|
304
|
+
spells: list[str] = Field(description="Spell names granted")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SubclassNarrative(BaseModel):
|
|
308
|
+
"""Narrative/flavor content for a subclass."""
|
|
309
|
+
|
|
310
|
+
subclass_name: str
|
|
311
|
+
description: str
|
|
312
|
+
feature_flavor: list[LevelFeatureNarrative] = Field(default_factory=list)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class SubclassGenerationOutput(BaseModel):
|
|
316
|
+
"""Complete LLM output for subclass generation."""
|
|
317
|
+
|
|
318
|
+
mechanics: SubclassMechanics
|
|
319
|
+
narrative: SubclassNarrative
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# =============================================================================
|
|
323
|
+
# BACKGROUND
|
|
324
|
+
# =============================================================================
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class BackgroundMechanics(BaseModel):
|
|
328
|
+
"""Independent mechanical values for a background.
|
|
329
|
+
|
|
330
|
+
Per D&D 2024 rules: 2 skills, 1 tool, ability score increases (+2/+1),
|
|
331
|
+
1 origin feat, starting equipment.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
skill_proficiencies: list[str] = Field(min_length=2, max_length=2)
|
|
335
|
+
tool_proficiency: str = ""
|
|
336
|
+
ability_score_increases: dict[AbilityName, int] = Field(
|
|
337
|
+
default_factory=dict,
|
|
338
|
+
description="Ability boosts, e.g. {'strength': 2, 'constitution': 1}",
|
|
339
|
+
)
|
|
340
|
+
origin_feat: str = Field(description="1st-level feat name")
|
|
341
|
+
equipment: list[str] = Field(default_factory=list)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class BackgroundNarrative(BaseModel):
|
|
345
|
+
"""Narrative/flavor content for a background."""
|
|
346
|
+
|
|
347
|
+
background_name: str
|
|
348
|
+
description: str
|
|
349
|
+
feature_name: str = ""
|
|
350
|
+
feature_description: str = Field(
|
|
351
|
+
default="", description="Roleplay benefit description"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class BackgroundGenerationOutput(BaseModel):
|
|
356
|
+
"""Complete LLM output for background generation."""
|
|
357
|
+
|
|
358
|
+
mechanics: BackgroundMechanics
|
|
359
|
+
narrative: BackgroundNarrative
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# =============================================================================
|
|
363
|
+
# CREATURE / MONSTER
|
|
364
|
+
# =============================================================================
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class CreatureMechanics(BaseModel):
|
|
368
|
+
"""Independent mechanical values for a monster/creature stat block.
|
|
369
|
+
|
|
370
|
+
The LLM decides these. The graph computes proficiency bonus (from CR),
|
|
371
|
+
save bonuses, skill bonuses, effective HP, etc.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
cr: str = Field(description="Challenge rating as string: '1/4', '5', '20'")
|
|
375
|
+
cr_numeric: float = Field(ge=0, le=30)
|
|
376
|
+
size: CreatureSize = "Medium"
|
|
377
|
+
creature_type: str = "Beast"
|
|
378
|
+
|
|
379
|
+
# Ability scores (all 6 are independent for creatures)
|
|
380
|
+
ability_scores: dict[AbilityName, int] = Field(
|
|
381
|
+
default_factory=lambda: {
|
|
382
|
+
"strength": 10, "dexterity": 10, "constitution": 10,
|
|
383
|
+
"intelligence": 10, "wisdom": 10, "charisma": 10,
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Defenses
|
|
388
|
+
ac: int = Field(10, ge=1, le=30, description="Armor class (includes natural armor)")
|
|
389
|
+
ac_type: str | None = Field(default=None, description="'natural armor', 'plate', etc.")
|
|
390
|
+
hp_dice_count: int = Field(1, ge=1, description="Number of hit dice")
|
|
391
|
+
# hit die SIZE is computed from size via CREATURE_SIZE_HIT_DIE lookup
|
|
392
|
+
|
|
393
|
+
# Movement
|
|
394
|
+
speed: SpeciesSpeed = Field(default_factory=lambda: SpeciesSpeed())
|
|
395
|
+
|
|
396
|
+
# Proficiencies (LLM picks which saves/skills are proficient)
|
|
397
|
+
saving_throw_proficiencies: list[AbilityName] = Field(default_factory=list)
|
|
398
|
+
skill_proficiencies: list[str] = Field(default_factory=list)
|
|
399
|
+
|
|
400
|
+
# Defenses
|
|
401
|
+
damage_vulnerabilities: list[str] = Field(default_factory=list)
|
|
402
|
+
damage_resistances: list[str] = Field(default_factory=list)
|
|
403
|
+
damage_immunities: list[str] = Field(default_factory=list)
|
|
404
|
+
condition_immunities: list[str] = Field(default_factory=list)
|
|
405
|
+
|
|
406
|
+
# Senses
|
|
407
|
+
darkvision: int | None = None
|
|
408
|
+
blindsight: int | None = None
|
|
409
|
+
tremorsense: int | None = None
|
|
410
|
+
truesight: int | None = None
|
|
411
|
+
|
|
412
|
+
# Languages
|
|
413
|
+
languages: list[str] = Field(default_factory=list)
|
|
414
|
+
|
|
415
|
+
# Actions (mechanical effects)
|
|
416
|
+
multiattack_count: int | None = Field(
|
|
417
|
+
default=None, description="Number of attacks in multiattack"
|
|
418
|
+
)
|
|
419
|
+
actions: list[CreatureActionMechanics] = Field(default_factory=list)
|
|
420
|
+
bonus_actions: list[CreatureActionMechanics] = Field(default_factory=list)
|
|
421
|
+
reactions: list[CreatureActionMechanics] = Field(default_factory=list)
|
|
422
|
+
traits: list[CreatureTraitMechanics] = Field(default_factory=list)
|
|
423
|
+
|
|
424
|
+
# Boss features
|
|
425
|
+
legendary_action_count: int = Field(default=0, ge=0, le=5)
|
|
426
|
+
legendary_actions: list[LegendaryActionMechanics] = Field(default_factory=list)
|
|
427
|
+
legendary_resistances: int = Field(default=0, ge=0, le=5)
|
|
428
|
+
lair_actions: list[LairActionMechanics] = Field(default_factory=list)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class CreatureActionMechanics(BaseModel):
|
|
432
|
+
"""Mechanical definition of a creature action."""
|
|
433
|
+
|
|
434
|
+
action_id: str
|
|
435
|
+
action_type: Literal["melee", "ranged", "spell", "other"] = "melee"
|
|
436
|
+
attack_bonus: int | None = Field(
|
|
437
|
+
default=None,
|
|
438
|
+
description="To-hit bonus. If None, no attack roll (save-based).",
|
|
439
|
+
)
|
|
440
|
+
reach_range: str | None = Field(default=None, description="e.g. '5 ft.' or '30/120 ft.'")
|
|
441
|
+
damage: str | None = Field(default=None, description="e.g. '2d6 + 4'")
|
|
442
|
+
damage_type: str | None = None
|
|
443
|
+
save_dc: int | None = Field(default=None, description="Save DC if save-based")
|
|
444
|
+
save_ability: AbilityName | None = None
|
|
445
|
+
recharge: str | None = Field(default=None, description="e.g. '5-6'")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class CreatureTraitMechanics(BaseModel):
|
|
449
|
+
"""Mechanical definition of a special trait."""
|
|
450
|
+
|
|
451
|
+
trait_id: str
|
|
452
|
+
recharge: str | None = None
|
|
453
|
+
modifies_node: list[NodeModifier] = Field(default_factory=list)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class LegendaryActionMechanics(BaseModel):
|
|
457
|
+
"""Mechanical definition of a legendary action."""
|
|
458
|
+
|
|
459
|
+
action_id: str
|
|
460
|
+
cost: int = Field(1, ge=1, le=3)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class LairActionMechanics(BaseModel):
|
|
464
|
+
"""Mechanical definition of a lair action."""
|
|
465
|
+
|
|
466
|
+
action_id: str
|
|
467
|
+
initiative_count: int = 20
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class CreatureNarrative(BaseModel):
|
|
471
|
+
"""Narrative/flavor content for a creature."""
|
|
472
|
+
|
|
473
|
+
name: str
|
|
474
|
+
description: str = Field(description="Physical appearance and general nature")
|
|
475
|
+
lore: str = Field(default="", description="Background/ecology")
|
|
476
|
+
tactics: str = Field(default="", description="How to run in combat")
|
|
477
|
+
alignment: str | None = None
|
|
478
|
+
action_descriptions: list[CreatureActionNarrative] = Field(default_factory=list)
|
|
479
|
+
trait_descriptions: list[CreatureTraitNarrative] = Field(default_factory=list)
|
|
480
|
+
legendary_action_descriptions: list[CreatureActionNarrative] = Field(
|
|
481
|
+
default_factory=list
|
|
482
|
+
)
|
|
483
|
+
lair_action_descriptions: list[LairActionNarrative] = Field(default_factory=list)
|
|
484
|
+
regional_effects: list[str] = Field(default_factory=list)
|
|
485
|
+
encounter_ideas: list[str] = Field(default_factory=list)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class CreatureActionNarrative(BaseModel):
|
|
489
|
+
"""Name and flavor for a creature action."""
|
|
490
|
+
|
|
491
|
+
action_id: str
|
|
492
|
+
name: str
|
|
493
|
+
description: str
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class CreatureTraitNarrative(BaseModel):
|
|
497
|
+
"""Name and flavor for a creature trait."""
|
|
498
|
+
|
|
499
|
+
trait_id: str
|
|
500
|
+
name: str
|
|
501
|
+
description: str
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class LairActionNarrative(BaseModel):
|
|
505
|
+
"""Flavor for a lair action."""
|
|
506
|
+
|
|
507
|
+
action_id: str
|
|
508
|
+
description: str
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class CreatureGenerationOutput(BaseModel):
|
|
512
|
+
"""Complete LLM output for creature/monster generation."""
|
|
513
|
+
|
|
514
|
+
mechanics: CreatureMechanics
|
|
515
|
+
narrative: CreatureNarrative
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# =============================================================================
|
|
519
|
+
# FEAT (mini-graph)
|
|
520
|
+
# =============================================================================
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class FeatMechanics(BaseModel):
|
|
524
|
+
"""Mechanical definition of a feat.
|
|
525
|
+
|
|
526
|
+
A feat is a mini-graph: it applies NodeModifiers to the character graph.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
feat_id: str
|
|
530
|
+
prerequisite_level: int = Field(default=1, ge=1, le=20)
|
|
531
|
+
prerequisite_ability: AbilityName | None = None
|
|
532
|
+
prerequisite_ability_min: int | None = None
|
|
533
|
+
grants_ability_increase: dict[AbilityName, int] = Field(
|
|
534
|
+
default_factory=dict,
|
|
535
|
+
description="e.g. {'dexterity': 1} for feats that give +1",
|
|
536
|
+
)
|
|
537
|
+
grants_proficiency: list[str] = Field(default_factory=list)
|
|
538
|
+
grants_resistance: list[str] = Field(default_factory=list)
|
|
539
|
+
modifies_node: list[NodeModifier] = Field(
|
|
540
|
+
default_factory=list,
|
|
541
|
+
description="Direct graph node modifications",
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class FeatNarrative(BaseModel):
|
|
546
|
+
"""Flavor for a feat."""
|
|
547
|
+
|
|
548
|
+
feat_id: str
|
|
549
|
+
name: str
|
|
550
|
+
description: str
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class FeatGenerationOutput(BaseModel):
|
|
554
|
+
"""Complete LLM output for feat generation."""
|
|
555
|
+
|
|
556
|
+
mechanics: FeatMechanics
|
|
557
|
+
narrative: FeatNarrative
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# =============================================================================
|
|
561
|
+
# EQUIPMENT ITEMS (mini-graphs)
|
|
562
|
+
# =============================================================================
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class WeaponMechanics(BaseModel):
|
|
566
|
+
"""Mechanical definition of a weapon."""
|
|
567
|
+
|
|
568
|
+
weapon_id: str
|
|
569
|
+
base_weapon: str = Field(description="Official weapon name, e.g. 'longsword'")
|
|
570
|
+
damage: str = Field(description="Damage dice, e.g. '1d8'")
|
|
571
|
+
damage_type: str = "slashing"
|
|
572
|
+
properties: list[str] = Field(
|
|
573
|
+
default_factory=list,
|
|
574
|
+
description="e.g. ['finesse', 'light', 'versatile']",
|
|
575
|
+
)
|
|
576
|
+
magic_bonus: int = Field(default=0, ge=0, le=3)
|
|
577
|
+
rarity: str = "common"
|
|
578
|
+
requires_attunement: bool = False
|
|
579
|
+
special_abilities: list[WeaponAbilityMechanics] = Field(default_factory=list)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class WeaponAbilityMechanics(BaseModel):
|
|
583
|
+
"""Mechanical effect of a magic weapon ability."""
|
|
584
|
+
|
|
585
|
+
ability_id: str
|
|
586
|
+
modifies_node: list[NodeModifier] = Field(default_factory=list)
|
|
587
|
+
uses_per_rest: int | None = None
|
|
588
|
+
recovery: Literal["short_rest", "long_rest"] | None = None
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class WeaponNarrative(BaseModel):
|
|
592
|
+
"""Flavor for a weapon."""
|
|
593
|
+
|
|
594
|
+
weapon_id: str
|
|
595
|
+
themed_name: str
|
|
596
|
+
description: str = ""
|
|
597
|
+
lore: str = ""
|
|
598
|
+
appearance: str = ""
|
|
599
|
+
symbolism: list[str] = Field(default_factory=list)
|
|
600
|
+
is_signature: bool = False
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class ArmorMechanics(BaseModel):
|
|
604
|
+
"""Mechanical definition of an armor piece.
|
|
605
|
+
|
|
606
|
+
The base AC and max DEX are looked up from the armor type.
|
|
607
|
+
Only magic_bonus is an independent variable beyond armor_type.
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
armor_id: str
|
|
611
|
+
armor_type: str = Field(description="e.g. 'plate', 'leather', 'chain_mail'")
|
|
612
|
+
magic_bonus: int = Field(default=0, ge=0, le=3)
|
|
613
|
+
rarity: str = "common"
|
|
614
|
+
requires_attunement: bool = False
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class ArmorNarrative(BaseModel):
|
|
618
|
+
"""Flavor for armor."""
|
|
619
|
+
|
|
620
|
+
armor_id: str
|
|
621
|
+
themed_name: str
|
|
622
|
+
description: str = ""
|
|
623
|
+
appearance: str = ""
|
|
624
|
+
is_signature: bool = False
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# =============================================================================
|
|
628
|
+
# SPELL (mini-graph for signature/innate spells)
|
|
629
|
+
# =============================================================================
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class SpellMechanics(BaseModel):
|
|
633
|
+
"""Mechanical definition of a spell."""
|
|
634
|
+
|
|
635
|
+
spell_id: str
|
|
636
|
+
base_spell: str = Field(description="Official D&D spell name or 'custom'")
|
|
637
|
+
level: int = Field(ge=0, le=9, description="0 = cantrip")
|
|
638
|
+
school: str = ""
|
|
639
|
+
casting_time: str = "1 action"
|
|
640
|
+
range: str = "Self"
|
|
641
|
+
components: list[str] = Field(default_factory=list, description="V, S, M")
|
|
642
|
+
duration: str = "Instantaneous"
|
|
643
|
+
concentration: bool = False
|
|
644
|
+
damage: str | None = Field(default=None, description="Damage dice if applicable")
|
|
645
|
+
damage_type: str | None = None
|
|
646
|
+
save_ability: AbilityName | None = None
|
|
647
|
+
is_signature: bool = False
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class SpellGenerationOutput(BaseModel):
|
|
651
|
+
"""Complete LLM output for a spell."""
|
|
652
|
+
|
|
653
|
+
mechanics: SpellMechanics
|
|
654
|
+
narrative: SpellNarrative
|