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.
@@ -0,0 +1,501 @@
1
+ """All D&D 5e 2024 lookup tables extracted from generation.py constants.
2
+
3
+ These tables are pure data — no logic, no code. They become part of the
4
+ Ruleset and are available to any formula operation via the `tables` parameter.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ # =============================================================================
10
+ # Armor
11
+ # =============================================================================
12
+
13
+ ARMOR_BASE_AC: dict[str, int] = {
14
+ "none": 10,
15
+ "unarmored": 10,
16
+ "padded": 11,
17
+ "leather": 11,
18
+ "studded": 12,
19
+ "studded_leather": 12,
20
+ "studded leather": 12,
21
+ "hide": 12,
22
+ "chain_shirt": 13,
23
+ "chain shirt": 13,
24
+ "scale_mail": 14,
25
+ "scale mail": 14,
26
+ "breastplate": 14,
27
+ "half_plate": 15,
28
+ "half plate": 15,
29
+ "ring_mail": 14,
30
+ "ring mail": 14,
31
+ "chain_mail": 16,
32
+ "chain mail": 16,
33
+ "splint": 17,
34
+ "plate": 18,
35
+ }
36
+
37
+ # Maximum DEX bonus allowed by armor type.
38
+ # None = unlimited (light/unarmored), 0 = no DEX bonus (heavy).
39
+ ARMOR_MAX_DEX: dict[str, int | None] = {
40
+ "none": None,
41
+ "unarmored": None,
42
+ "padded": None,
43
+ "leather": None,
44
+ "studded": None,
45
+ "studded_leather": None,
46
+ "studded leather": None,
47
+ "hide": 2,
48
+ "chain_shirt": 2,
49
+ "chain shirt": 2,
50
+ "scale_mail": 2,
51
+ "scale mail": 2,
52
+ "breastplate": 2,
53
+ "half_plate": 2,
54
+ "half plate": 2,
55
+ "ring_mail": 0,
56
+ "ring mail": 0,
57
+ "chain_mail": 0,
58
+ "chain mail": 0,
59
+ "splint": 0,
60
+ "plate": 0,
61
+ }
62
+
63
+ # =============================================================================
64
+ # Skill → Ability mapping
65
+ # =============================================================================
66
+
67
+ SKILL_ABILITY_MAP: dict[str, str] = {
68
+ "acrobatics": "dexterity",
69
+ "animal_handling": "wisdom",
70
+ "arcana": "intelligence",
71
+ "athletics": "strength",
72
+ "deception": "charisma",
73
+ "history": "intelligence",
74
+ "insight": "wisdom",
75
+ "intimidation": "charisma",
76
+ "investigation": "intelligence",
77
+ "medicine": "wisdom",
78
+ "nature": "intelligence",
79
+ "perception": "wisdom",
80
+ "performance": "charisma",
81
+ "persuasion": "charisma",
82
+ "religion": "intelligence",
83
+ "sleight_of_hand": "dexterity",
84
+ "stealth": "dexterity",
85
+ "survival": "wisdom",
86
+ }
87
+
88
+ # =============================================================================
89
+ # Spellcasting ability by class
90
+ # =============================================================================
91
+
92
+ SPELL_ABILITY_BY_CLASS: dict[str, str] = {
93
+ "wizard": "intelligence",
94
+ "artificer": "intelligence",
95
+ "cleric": "wisdom",
96
+ "druid": "wisdom",
97
+ "ranger": "wisdom",
98
+ "monk": "wisdom",
99
+ "bard": "charisma",
100
+ "paladin": "charisma",
101
+ "sorcerer": "charisma",
102
+ "warlock": "charisma",
103
+ }
104
+
105
+ # =============================================================================
106
+ # Hit die by class
107
+ # =============================================================================
108
+
109
+ HIT_DIE_BY_CLASS: dict[str, int] = {
110
+ "barbarian": 12,
111
+ "fighter": 10,
112
+ "paladin": 10,
113
+ "ranger": 10,
114
+ "monk": 8,
115
+ "rogue": 8,
116
+ "bard": 8,
117
+ "cleric": 8,
118
+ "druid": 8,
119
+ "warlock": 8,
120
+ "artificer": 8,
121
+ "wizard": 6,
122
+ "sorcerer": 6,
123
+ }
124
+
125
+ # Hit die by archetype (fallback when class name not available)
126
+ HIT_DIE_BY_ARCHETYPE: dict[str, int] = {
127
+ "full_martial": 10,
128
+ "skill_martial": 8,
129
+ "primal_martial": 12,
130
+ "half_caster": 10,
131
+ "full_caster": 6,
132
+ "pact_caster": 8,
133
+ "support_caster": 8,
134
+ "gish": 8,
135
+ "tank": 10,
136
+ "martial": 10,
137
+ }
138
+
139
+ # =============================================================================
140
+ # Spell slot progression tables
141
+ # =============================================================================
142
+
143
+ # Full casters: Wizard, Cleric, Druid, Sorcerer, Bard
144
+ SPELL_SLOTS_FULL: dict[int, dict[str, int]] = {
145
+ 1: {"1st": 2},
146
+ 2: {"1st": 3},
147
+ 3: {"1st": 4, "2nd": 2},
148
+ 4: {"1st": 4, "2nd": 3},
149
+ 5: {"1st": 4, "2nd": 3, "3rd": 2},
150
+ 6: {"1st": 4, "2nd": 3, "3rd": 3},
151
+ 7: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 1},
152
+ 8: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 2},
153
+ 9: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 1},
154
+ 10: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2},
155
+ 11: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1},
156
+ 12: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1},
157
+ 13: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1, "7th": 1},
158
+ 14: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1, "7th": 1},
159
+ 15: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1, "7th": 1, "8th": 1},
160
+ 16: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1, "7th": 1, "8th": 1},
161
+ 17: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2, "6th": 1, "7th": 1, "8th": 1, "9th": 1},
162
+ 18: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 3, "6th": 1, "7th": 1, "8th": 1, "9th": 1},
163
+ 19: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 3, "6th": 2, "7th": 1, "8th": 1, "9th": 1},
164
+ 20: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 3, "6th": 2, "7th": 2, "8th": 1, "9th": 1},
165
+ }
166
+
167
+ # Half casters: Paladin, Ranger — start at level 2
168
+ SPELL_SLOTS_HALF: dict[int, dict[str, int]] = {
169
+ 1: {},
170
+ 2: {"1st": 2},
171
+ 3: {"1st": 3},
172
+ 4: {"1st": 3},
173
+ 5: {"1st": 4, "2nd": 2},
174
+ 6: {"1st": 4, "2nd": 2},
175
+ 7: {"1st": 4, "2nd": 3},
176
+ 8: {"1st": 4, "2nd": 3},
177
+ 9: {"1st": 4, "2nd": 3, "3rd": 2},
178
+ 10: {"1st": 4, "2nd": 3, "3rd": 2},
179
+ 11: {"1st": 4, "2nd": 3, "3rd": 3},
180
+ 12: {"1st": 4, "2nd": 3, "3rd": 3},
181
+ 13: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 1},
182
+ 14: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 1},
183
+ 15: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 2},
184
+ 16: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 2},
185
+ 17: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 1},
186
+ 18: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 1},
187
+ 19: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2},
188
+ 20: {"1st": 4, "2nd": 3, "3rd": 3, "4th": 3, "5th": 2},
189
+ }
190
+
191
+ # Warlock pact magic — separate system
192
+ SPELL_SLOTS_WARLOCK: dict[int, dict[str, int]] = {
193
+ 1: {"pact": 1, "pact_level": 1},
194
+ 2: {"pact": 2, "pact_level": 1},
195
+ 3: {"pact": 2, "pact_level": 2},
196
+ 4: {"pact": 2, "pact_level": 2},
197
+ 5: {"pact": 2, "pact_level": 3},
198
+ 6: {"pact": 2, "pact_level": 3},
199
+ 7: {"pact": 2, "pact_level": 4},
200
+ 8: {"pact": 2, "pact_level": 4},
201
+ 9: {"pact": 2, "pact_level": 5},
202
+ 10: {"pact": 2, "pact_level": 5},
203
+ 11: {"pact": 3, "pact_level": 5},
204
+ 12: {"pact": 3, "pact_level": 5},
205
+ 13: {"pact": 3, "pact_level": 5},
206
+ 14: {"pact": 3, "pact_level": 5},
207
+ 15: {"pact": 3, "pact_level": 5},
208
+ 16: {"pact": 3, "pact_level": 5},
209
+ 17: {"pact": 4, "pact_level": 5},
210
+ 18: {"pact": 4, "pact_level": 5},
211
+ 19: {"pact": 4, "pact_level": 5},
212
+ 20: {"pact": 4, "pact_level": 5},
213
+ }
214
+
215
+ # =============================================================================
216
+ # Spellcasting type by class
217
+ # =============================================================================
218
+
219
+ SPELLCASTING_TYPE_BY_CLASS: dict[str, str] = {
220
+ "wizard": "full_caster",
221
+ "cleric": "full_caster",
222
+ "druid": "full_caster",
223
+ "sorcerer": "full_caster",
224
+ "bard": "full_caster",
225
+ "paladin": "half_caster",
226
+ "ranger": "half_caster",
227
+ "artificer": "half_caster",
228
+ "warlock": "pact_caster",
229
+ "fighter": "none",
230
+ "barbarian": "none",
231
+ "rogue": "none",
232
+ "monk": "none",
233
+ }
234
+
235
+ # =============================================================================
236
+ # XP thresholds
237
+ # =============================================================================
238
+
239
+ XP_THRESHOLDS: dict[int, int] = {
240
+ 1: 0,
241
+ 2: 300,
242
+ 3: 900,
243
+ 4: 2700,
244
+ 5: 6500,
245
+ 6: 14000,
246
+ 7: 23000,
247
+ 8: 34000,
248
+ 9: 48000,
249
+ 10: 64000,
250
+ 11: 85000,
251
+ 12: 100000,
252
+ 13: 120000,
253
+ 14: 140000,
254
+ 15: 165000,
255
+ 16: 195000,
256
+ 17: 225000,
257
+ 18: 265000,
258
+ 19: 305000,
259
+ 20: 355000,
260
+ }
261
+
262
+ # =============================================================================
263
+ # Item rarity unlock requirements
264
+ # =============================================================================
265
+
266
+ RARITY_UNLOCK_REQUIREMENTS: dict[str, dict[str, int]] = {
267
+ "common": {"min_level": 1, "min_xp": 0},
268
+ "uncommon": {"min_level": 1, "min_xp": 0},
269
+ "rare": {"min_level": 5, "min_xp": 6500},
270
+ "very_rare": {"min_level": 11, "min_xp": 85000},
271
+ "legendary": {"min_level": 17, "min_xp": 225000},
272
+ "artifact": {"min_level": 17, "min_xp": 225000},
273
+ }
274
+
275
+ # Simple level-only lookup (convenience alias)
276
+ RARITY_LEVEL_REQUIREMENTS: dict[str, int] = {
277
+ rarity: req["min_level"] for rarity, req in RARITY_UNLOCK_REQUIREMENTS.items()
278
+ }
279
+
280
+ # =============================================================================
281
+ # Weapon mastery (D&D 2024)
282
+ # =============================================================================
283
+
284
+ WEAPON_MASTERY_MAP: dict[str, str] = {
285
+ # Simple melee
286
+ "club": "Slow",
287
+ "dagger": "Nick",
288
+ "greatclub": "Push",
289
+ "handaxe": "Vex",
290
+ "javelin": "Slow",
291
+ "light hammer": "Nick",
292
+ "mace": "Sap",
293
+ "quarterstaff": "Topple",
294
+ "sickle": "Nick",
295
+ "spear": "Sap",
296
+ # Simple ranged
297
+ "dart": "Vex",
298
+ "light crossbow": "Slow",
299
+ "shortbow": "Vex",
300
+ "sling": "Slow",
301
+ # Martial melee
302
+ "battleaxe": "Topple",
303
+ "flail": "Sap",
304
+ "glaive": "Graze",
305
+ "greataxe": "Cleave",
306
+ "greatsword": "Graze",
307
+ "halberd": "Cleave",
308
+ "lance": "Topple",
309
+ "longsword": "Sap",
310
+ "maul": "Topple",
311
+ "morningstar": "Sap",
312
+ "pike": "Push",
313
+ "rapier": "Vex",
314
+ "scimitar": "Nick",
315
+ "shortsword": "Vex",
316
+ "trident": "Topple",
317
+ "war pick": "Sap",
318
+ "warhammer": "Push",
319
+ "whip": "Slow",
320
+ # Martial ranged
321
+ "blowgun": "Vex",
322
+ "hand crossbow": "Vex",
323
+ "heavy crossbow": "Push",
324
+ "longbow": "Slow",
325
+ "musket": "Slow",
326
+ "pistol": "Vex",
327
+ }
328
+
329
+ # D&D 2024 weapon mastery rule text — paraphrased from PHB pp. 213-214,
330
+ # keyed by the canonical mastery label that WEAPON_MASTERY_MAP emits.
331
+ # Used by adapters.py::_build_equipment_info to surface the effect in
332
+ # the character-sheet response. An unknown key MUST
333
+ # surface as `mastery_unknown: true` rather than silently empty
334
+ # description.
335
+ WEAPON_MASTERY_DESCRIPTIONS: dict[str, str] = {
336
+ "Cleave": (
337
+ "After hitting a creature with a melee attack, you can make "
338
+ "a melee attack with the same weapon against a second "
339
+ "creature within 5 feet of the first, using the same attack "
340
+ "roll. The second target must be within the weapon's reach. "
341
+ "You can use this once per turn."
342
+ ),
343
+ "Graze": (
344
+ "If your attack roll against a creature misses, you can deal "
345
+ "damage to that creature equal to the ability modifier used "
346
+ "to make the attack roll. The damage is the same type as the "
347
+ "weapon's damage. Doesn't apply on a critical miss."
348
+ ),
349
+ "Nick": (
350
+ "When you make the extra attack of the Light property, you "
351
+ "can make it as part of the Attack action instead of as a "
352
+ "Bonus Action. You can make this extra attack only once per "
353
+ "turn."
354
+ ),
355
+ "Push": (
356
+ "When you hit a creature with this weapon, you can push the "
357
+ "creature up to 10 feet straight away from you if it's Large "
358
+ "or smaller."
359
+ ),
360
+ "Sap": (
361
+ "When you hit a creature with this weapon, that creature has "
362
+ "Disadvantage on its next attack roll before the start of "
363
+ "your next turn."
364
+ ),
365
+ "Slow": (
366
+ "When you hit a creature with this weapon and deal damage, "
367
+ "you can reduce that creature's Speed by 10 feet until the "
368
+ "start of your next turn. If the creature is hit more than "
369
+ "once, the speed reduction doesn't stack."
370
+ ),
371
+ "Topple": (
372
+ "When you hit a creature with this weapon, you can force the "
373
+ "creature to make a Constitution saving throw (DC 8 + your "
374
+ "proficiency bonus + the ability modifier used to make the "
375
+ "attack roll). On a failed save, the creature has the Prone "
376
+ "condition."
377
+ ),
378
+ "Vex": (
379
+ "When you hit a creature with this weapon and deal damage, "
380
+ "you have Advantage on your next attack roll against that "
381
+ "creature before the end of your next turn."
382
+ ),
383
+ }
384
+
385
+ # =============================================================================
386
+ # Archetype-based proficiency defaults
387
+ # =============================================================================
388
+
389
+ ARCHETYPE_WEAPON_PROFICIENCIES: dict[str, list[str]] = {
390
+ "full_martial": ["Simple weapons", "Martial weapons"],
391
+ "skill_martial": ["Simple weapons", "Martial weapons"],
392
+ "half_caster": ["Simple weapons", "Martial weapons"],
393
+ "full_caster": ["Daggers", "Darts", "Slings", "Quarterstaffs", "Light crossbows"],
394
+ "support_caster": ["Simple weapons"],
395
+ "gish": ["Simple weapons", "Martial weapons"],
396
+ }
397
+
398
+ ARCHETYPE_ARMOR_PROFICIENCIES: dict[str, list[str]] = {
399
+ "full_martial": ["Light armor", "Medium armor", "Heavy armor", "Shields"],
400
+ "skill_martial": ["Light armor", "Medium armor"],
401
+ "half_caster": ["Light armor", "Medium armor", "Shields"],
402
+ "full_caster": [],
403
+ "support_caster": ["Light armor"],
404
+ "gish": ["Light armor", "Medium armor"],
405
+ }
406
+
407
+ # =============================================================================
408
+ # Saving throw proficiencies by class
409
+ # =============================================================================
410
+
411
+ SAVE_PROFICIENCIES_BY_CLASS: dict[str, list[str]] = {
412
+ "barbarian": ["strength", "constitution"],
413
+ "bard": ["dexterity", "charisma"],
414
+ "cleric": ["wisdom", "charisma"],
415
+ "druid": ["intelligence", "wisdom"],
416
+ "fighter": ["strength", "constitution"],
417
+ "monk": ["strength", "dexterity"],
418
+ "paladin": ["wisdom", "charisma"],
419
+ "ranger": ["strength", "dexterity"],
420
+ "rogue": ["dexterity", "intelligence"],
421
+ "sorcerer": ["constitution", "charisma"],
422
+ "warlock": ["wisdom", "charisma"],
423
+ "wizard": ["intelligence", "wisdom"],
424
+ "artificer": ["constitution", "intelligence"],
425
+ }
426
+
427
+ # =============================================================================
428
+ # Monster/creature stat block tables (for future creature computation graph)
429
+ # =============================================================================
430
+
431
+ # Challenge Rating to Proficiency Bonus
432
+ CR_PROFICIENCY_BONUS: dict[str, int] = {
433
+ "0": 2,
434
+ "1/8": 2,
435
+ "1/4": 2,
436
+ "1/2": 2,
437
+ "1": 2,
438
+ "2": 2,
439
+ "3": 2,
440
+ "4": 2,
441
+ "5": 3,
442
+ "6": 3,
443
+ "7": 3,
444
+ "8": 3,
445
+ "9": 4,
446
+ "10": 4,
447
+ "11": 4,
448
+ "12": 4,
449
+ "13": 5,
450
+ "14": 5,
451
+ "15": 5,
452
+ "16": 5,
453
+ "17": 6,
454
+ "18": 6,
455
+ "19": 6,
456
+ "20": 6,
457
+ "21": 7,
458
+ "22": 7,
459
+ "23": 7,
460
+ "24": 7,
461
+ "25": 8,
462
+ "26": 8,
463
+ "27": 8,
464
+ "28": 8,
465
+ "29": 9,
466
+ "30": 9,
467
+ }
468
+
469
+ # Size categories to hit die size
470
+ CREATURE_SIZE_HIT_DIE: dict[str, int] = {
471
+ "tiny": 4,
472
+ "small": 6,
473
+ "medium": 8,
474
+ "large": 10,
475
+ "huge": 12,
476
+ "gargantuan": 20,
477
+ }
478
+
479
+
480
+ def get_all_lookup_tables() -> dict:
481
+ """Return all lookup tables as a flat dict for use in a Ruleset."""
482
+ return {
483
+ "armor_base_ac": ARMOR_BASE_AC,
484
+ "armor_max_dex": ARMOR_MAX_DEX,
485
+ "skill_ability_map": SKILL_ABILITY_MAP,
486
+ "spell_ability_by_class": SPELL_ABILITY_BY_CLASS,
487
+ "hit_die_by_class": HIT_DIE_BY_CLASS,
488
+ "hit_die_by_archetype": HIT_DIE_BY_ARCHETYPE,
489
+ "spell_slots_full": SPELL_SLOTS_FULL,
490
+ "spell_slots_half": SPELL_SLOTS_HALF,
491
+ "spell_slots_warlock": SPELL_SLOTS_WARLOCK,
492
+ "spellcasting_type_by_class": SPELLCASTING_TYPE_BY_CLASS,
493
+ "xp_thresholds": XP_THRESHOLDS,
494
+ "rarity_unlock_requirements": RARITY_UNLOCK_REQUIREMENTS,
495
+ "weapon_mastery_map": WEAPON_MASTERY_MAP,
496
+ "archetype_weapon_proficiencies": ARCHETYPE_WEAPON_PROFICIENCIES,
497
+ "archetype_armor_proficiencies": ARCHETYPE_ARMOR_PROFICIENCIES,
498
+ "save_proficiencies_by_class": SAVE_PROFICIENCIES_BY_CLASS,
499
+ "cr_proficiency_bonus": CR_PROFICIENCY_BONUS,
500
+ "creature_size_hit_die": CREATURE_SIZE_HIT_DIE,
501
+ }