dnd5e-engine 0.3.2__tar.gz → 0.4.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.
Files changed (160) hide show
  1. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/CHANGELOG.md +36 -0
  2. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/PKG-INFO +1 -1
  3. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/pyproject.toml +7 -1
  4. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/__init__.py +22 -1
  5. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/death_saves.py +92 -1
  6. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/dispatch.py +13 -0
  7. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/event_dicts.py +13 -0
  8. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/orchestrator.py +1 -2
  9. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/combat.py +15 -1
  10. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/combat_data.py +13 -0
  11. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/combat_helpers.py +14 -90
  12. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/equipment.py +13 -0
  13. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/gambits.py +14 -7
  14. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/resolution.py +13 -0
  15. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/spells.py +15 -1
  16. dnd5e_engine-0.4.0/src/dnd5e_engine/types/__init__.py +49 -0
  17. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/combat.py +10 -0
  18. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/dice.py +15 -1
  19. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/intent.py +13 -0
  20. dnd5e_engine-0.4.0/tests/test_gen1_deprecation_warnings.py +63 -0
  21. dnd5e_engine-0.4.0/tests/test_gen2_has_no_gen1_imports.py +52 -0
  22. dnd5e_engine-0.4.0/tests/test_generation_parity_attack.py +179 -0
  23. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_public_api_surface.py +9 -4
  24. dnd5e_engine-0.3.2/src/dnd5e_engine/types/__init__.py +0 -24
  25. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/.gitignore +0 -0
  26. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/LICENSE +0 -0
  27. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/Makefile +0 -0
  28. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/NOTICE +0 -0
  29. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/README.md +0 -0
  30. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/scripts/_smoke_grid_combat.py +0 -0
  31. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/scripts/smoke_clean_install.sh +0 -0
  32. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/__init__.py +0 -0
  33. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/apply.py +0 -0
  34. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/attack.py +0 -0
  35. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/build_context.py +0 -0
  36. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/cast.py +0 -0
  37. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/check.py +0 -0
  38. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/context.py +0 -0
  39. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/damage.py +0 -0
  40. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/dice.py +0 -0
  41. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/effects.py +0 -0
  42. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/formula.py +0 -0
  43. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/heal.py +0 -0
  44. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/mastery.py +0 -0
  45. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/monster_actions.py +0 -0
  46. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/passive_stats.py +0 -0
  47. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/resolver.py +0 -0
  48. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/save.py +0 -0
  49. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/save_primitive.py +0 -0
  50. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/scale.py +0 -0
  51. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/build_party.py +0 -0
  52. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/build_spec.py +0 -0
  53. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/check.py +0 -0
  54. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/events.py +0 -0
  55. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/lib_loader.py +0 -0
  56. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/outcome.py +0 -0
  57. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/py.typed +0 -0
  58. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rest.py +0 -0
  59. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/results.py +0 -0
  60. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/__init__.py +0 -0
  61. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/_class_meta.py +0 -0
  62. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/_parsing.py +0 -0
  63. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/conditions.py +0 -0
  64. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/dice.py +0 -0
  65. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/effects.py +0 -0
  66. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/skills.py +0 -0
  67. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/spatial.py +0 -0
  68. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/specs.py +0 -0
  69. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/testing.py +0 -0
  70. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/conditions.py +0 -0
  71. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/effects.py +0 -0
  72. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/src/dnd5e_engine/views.py +0 -0
  73. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/__init__.py +0 -0
  74. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/__init__.py +0 -0
  75. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_build_context.py +0 -0
  76. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_cast_level_override.py +0 -0
  77. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_monster_actions.py +0 -0
  78. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_multiattack_fidelity.py +0 -0
  79. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_resolve_check_activity.py +0 -0
  80. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_save_primitive.py +0 -0
  81. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/activities/test_weapon_mastery.py +0 -0
  82. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/__init__.py +0 -0
  83. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/conftest.py +0 -0
  84. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/harness.py +0 -0
  85. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
  86. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
  87. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
  88. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c03_effect_modes.py +0 -0
  89. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
  90. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c05_spatial.py +0 -0
  91. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c06_reactions.py +0 -0
  92. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c07_sneak_and_repertoire.py +0 -0
  93. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c08_passive_projection.py +0 -0
  94. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c09_rest.py +0 -0
  95. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
  96. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/e2e/test_c11_item_charges.py +0 -0
  97. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_active_effect_resistance_fold.py +0 -0
  98. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_active_effect_schema.py +0 -0
  99. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_apply_changes_to_check.py +0 -0
  100. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_apply_modifiers.py +0 -0
  101. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_build_context_abilities.py +0 -0
  102. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_build_party.py +0 -0
  103. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_build_party_passives.py +0 -0
  104. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_capability_matrix.py +0 -0
  105. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_combat_bucket_keys.py +0 -0
  106. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_combat_flag_advantage.py +0 -0
  107. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_concentration_drop_effect_name.py +0 -0
  108. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_concentration_repeat_save_identity.py +0 -0
  109. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_cover_bonus_sidecar.py +0 -0
  110. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_coverage_backfill_death_saves.py +0 -0
  111. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
  112. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_determinism_contract.py +0 -0
  113. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_docstrings_are_host_agnostic.py +0 -0
  114. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_duration_tick_at_turn_end.py +0 -0
  115. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_effects_builder_internals.py +0 -0
  116. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_enchantment_sidecar_projection.py +0 -0
  117. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_end_combat_snapshot.py +0 -0
  118. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_expire_clears_active_conditions.py +0 -0
  119. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
  120. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_get_actor_active_effects.py +0 -0
  121. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_grid_topology.py +0 -0
  122. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_iter11_fixes.py +0 -0
  123. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_iter13_fixes.py +0 -0
  124. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_iter5_fixes.py +0 -0
  125. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_lib_loader.py +0 -0
  126. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_lib_loader_configure.py +0 -0
  127. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_live_combat_view.py +0 -0
  128. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_monster_dash.py +0 -0
  129. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_monster_flee_retreat.py +0 -0
  130. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_orchestrator_gating_typed.py +0 -0
  131. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_orchestrator_grid_combat.py +0 -0
  132. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_orchestrator_item_charges.py +0 -0
  133. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_orchestrator_monster_typed.py +0 -0
  134. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
  135. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_orchestrator_pc_resolution_typed.py +0 -0
  136. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_party_member_reach_ft.py +0 -0
  137. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_passive_stats.py +0 -0
  138. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_phase6_public_surface.py +0 -0
  139. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rage_second_wind_e2e.py +0 -0
  140. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_reaction_queue.py +0 -0
  141. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_resolve_check.py +0 -0
  142. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_resolve_check_flag_advantage.py +0 -0
  143. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_resolve_check_specific_buckets.py +0 -0
  144. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rest.py +0 -0
  145. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_combat_data_and_skills.py +0 -0
  146. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_combat_helpers.py +0 -0
  147. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_combat_resolution.py +0 -0
  148. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_conditions.py +0 -0
  149. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_dice_equipment_spells.py +0 -0
  150. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_effects_and_gambits.py +0 -0
  151. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_resolution_and_parsing.py +0 -0
  152. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_rules_smoke.py +0 -0
  153. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_scale_resolver.py +0 -0
  154. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_smoke.py +0 -0
  155. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_sneak_attack_resolver.py +0 -0
  156. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_start_combat_active_effects.py +0 -0
  157. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
  158. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_types_smoke.py +0 -0
  159. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_use_feature_intent.py +0 -0
  160. {dnd5e_engine-0.3.2 → dnd5e_engine-0.4.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0]
11
+
12
+ Lockstep release with `dnd5e-srd-data` 0.4.0 and `nat20-bridge` 0.4.0.
13
+
14
+ Deprecates the legacy (Gen 1) rules surface. **Nothing is removed**; every
15
+ deprecated import warns. Removal lands in 0.5.0 — see
16
+ `docs/migration/v0.3-to-v0.4.md`.
17
+
18
+ ### Deprecated
19
+
20
+ - **The legacy (Gen 1) rules surface — removed in 0.5.0.** `dispatch`,
21
+ `event_dicts`, `types.dice`, `types.intent`, and `rules.{combat, combat_data,
22
+ combat_helpers, equipment, gambits, resolution, spells}` emit a
23
+ `DeprecationWarning` on import. The top-level `dnd5e_engine.ActionType` and
24
+ `dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
25
+ re-exports now resolve lazily and warn on access. Every symbol's supported
26
+ route is in `docs/migration/v0.3-to-v0.4.md`. Rationale: two drifting
27
+ implementations of the same SRD rules, a non-seedable legacy path, and host
28
+ transport/persistence shapes inside a zero-I/O package.
29
+
30
+ ### Changed
31
+
32
+ - `DeathSaveState` canonical home is now `dnd5e_engine.death_saves`;
33
+ `BehaviorProfile` is now `dnd5e_engine.types.combat`. The old locations
34
+ re-export until 0.5.0. The supported engine's import tree no longer reaches
35
+ any legacy module (pinned by `tests/test_gen2_has_no_gen1_imports.py`).
36
+
37
+ ### Added
38
+
39
+ - Cross-generation attack-roll parity test
40
+ (`tests/test_generation_parity_attack.py`): `rules/combat.attack_roll` /
41
+ `resolve_player_attack` and `activities/attack.py` are fed the same d20
42
+ stream and must keep the same natural die and agree on hit / crit / miss
43
+ under advantage, disadvantage and flat rolls. This is the guard that would
44
+ have caught the drift fixed in 0.3.1 (#11). No runtime behavior change.
45
+
10
46
  ## [0.3.2]
11
47
 
12
48
  Lockstep release with `dnd5e-srd-data` 0.3.2 and `nat20-bridge` 0.3.2.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dnd5e-engine
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Pure-Python D&D 5e SRD rules engine — combat, checks, effects. Host-agnostic, zero I/O.
5
5
  Author: Tapestria contributors
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dnd5e-engine"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "Pure-Python D&D 5e SRD rules engine — combat, checks, effects. Host-agnostic, zero I/O."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -87,6 +87,12 @@ max-complexity = 23
87
87
 
88
88
  [tool.pytest.ini_options]
89
89
  testpaths = ["tests"]
90
+ filterwarnings = [
91
+ # Any DeprecationWarning the engine itself raises is a bug — except the
92
+ # 0.4.0 legacy-surface deprecations, which the suite exercises on purpose.
93
+ "error::DeprecationWarning:dnd5e_engine",
94
+ "ignore:.*legacy \\(Gen 1\\) surface.*:DeprecationWarning",
95
+ ]
90
96
 
91
97
  [tool.coverage.run]
92
98
  source = ["dnd5e_engine"]
@@ -79,7 +79,6 @@ from dnd5e_engine.types.effects import (
79
79
  ActiveEffectChange,
80
80
  ActiveEffectDuration,
81
81
  )
82
- from dnd5e_engine.types.intent import ActionType
83
82
 
84
83
  __all__ = [
85
84
  "AbilityScores",
@@ -130,3 +129,25 @@ __all__ = [
130
129
  "start_combat",
131
130
  "submit_player_intent",
132
131
  ]
132
+
133
+ _GEN1_LAZY = {"ActionType": ("dnd5e_engine.types.intent", "ActionType")}
134
+
135
+
136
+ def __getattr__(name: str) -> object:
137
+ # PEP 562 — keep the 0.3.x names resolvable without importing the
138
+ # deprecated modules eagerly (which would warn on every ``import dnd5e_engine``).
139
+ if name in _GEN1_LAZY:
140
+ import importlib
141
+ import warnings
142
+
143
+ module, attr = _GEN1_LAZY[name]
144
+ warnings.warn(
145
+ f"dnd5e_engine.{name} belongs to the legacy (Gen 1) surface — a host-side "
146
+ f"shape, not an engine type — and will be removed in dnd5e-engine 0.5.0. "
147
+ f"Import it from {module} (also deprecated) or copy it into your host; "
148
+ f"see docs/migration/v0.3-to-v0.4.md.",
149
+ DeprecationWarning,
150
+ stacklevel=2,
151
+ )
152
+ return getattr(importlib.import_module(module), attr)
153
+ raise AttributeError(f"module 'dnd5e_engine' has no attribute {name!r}")
@@ -22,6 +22,7 @@ from __future__ import annotations
22
22
 
23
23
  import random
24
24
  from dataclasses import dataclass
25
+ from typing import Any
25
26
 
26
27
  from dnd5e_engine.events import (
27
28
  CombatEvent,
@@ -30,9 +31,98 @@ from dnd5e_engine.events import (
30
31
  DeathSaveStarted,
31
32
  Stabilized,
32
33
  )
33
- from dnd5e_engine.rules.combat_helpers import DeathSaveState
34
34
  from dnd5e_engine.types.combat import Combatant
35
35
 
36
+
37
+ @dataclass
38
+ class DeathSaveState:
39
+ """Mutable state machine for D&D 5e death saving throws.
40
+
41
+ Designed for host persistence via to_dict / from_dict.
42
+ """
43
+
44
+ successes: int = 0
45
+ failures: int = 0
46
+ is_stable: bool = False
47
+
48
+ # ------------------------------------------------------------------
49
+ # Core state transitions
50
+ # ------------------------------------------------------------------
51
+
52
+ def apply_save(self, success: bool, is_critical: bool) -> str:
53
+ """Apply a death saving throw result.
54
+
55
+ Returns one of: "critical_success" | "stabilized" | "dead" | "ongoing".
56
+
57
+ Rules:
58
+ - Nat 20 (success=True, is_critical=True): regain 1 HP -> "critical_success"
59
+ - Nat 1 (success=False, is_critical=True): 2 failures
60
+ - Normal success: 1 success; 3 successes -> "stabilized"
61
+ - Normal failure: 1 failure; 3 failures -> "dead"
62
+ """
63
+ # Nat 20 takes priority — regain 1 HP, no counter update needed
64
+ if success and is_critical:
65
+ return "critical_success"
66
+
67
+ if success:
68
+ self.successes += 1
69
+ elif is_critical:
70
+ # Nat 1 counts as 2 failures
71
+ self.failures += 2
72
+ else:
73
+ self.failures += 1
74
+
75
+ return self._check_outcome()
76
+
77
+ def apply_damage_while_unconscious(self, is_melee_within_5ft: bool) -> str:
78
+ """Apply death save failures from damage while unconscious.
79
+
80
+ D&D 5e RAW: taking any damage while at 0 HP is a death save failure.
81
+ Melee attack within 5 ft = auto-crit = 2 failures. Otherwise 1 failure.
82
+
83
+ Returns "dead" if 3+ failures reached, else "ongoing".
84
+ """
85
+ self.failures += 2 if is_melee_within_5ft else 1
86
+ return self._check_outcome()
87
+
88
+ def reset(self) -> None:
89
+ """Reset all counters (called when character regains HP via nat 20)."""
90
+ self.successes = 0
91
+ self.failures = 0
92
+ self.is_stable = False
93
+
94
+ # ------------------------------------------------------------------
95
+ # Serialization (plain dicts for host persistence)
96
+ # ------------------------------------------------------------------
97
+
98
+ def to_dict(self) -> dict[str, Any]:
99
+ return {
100
+ "successes": self.successes,
101
+ "failures": self.failures,
102
+ "is_stable": self.is_stable,
103
+ }
104
+
105
+ @classmethod
106
+ def from_dict(cls, d: dict[str, Any]) -> DeathSaveState:
107
+ return cls(
108
+ successes=d.get("successes", 0),
109
+ failures=d.get("failures", 0),
110
+ is_stable=d.get("is_stable", False),
111
+ )
112
+
113
+ # ------------------------------------------------------------------
114
+ # Internal helpers
115
+ # ------------------------------------------------------------------
116
+
117
+ def _check_outcome(self) -> str:
118
+ if self.failures >= 3:
119
+ return "dead"
120
+ if self.successes >= 3:
121
+ self.is_stable = True
122
+ return "stabilized"
123
+ return "ongoing"
124
+
125
+
36
126
  # Outcome literal returned alongside events/state for the caller to drive
37
127
  # turn skip / combat-end / consciousness restore.
38
128
  DeathSaveOutcome = str # "ongoing" | "stabilized" | "dead" | "critical_success"
@@ -150,6 +240,7 @@ def reset_death_saves(combatant: Combatant) -> Combatant:
150
240
  __all__ = [
151
241
  "DeathSaveOutcome",
152
242
  "DeathSaveResult",
243
+ "DeathSaveState",
153
244
  "reset_death_saves",
154
245
  "roll_death_save",
155
246
  ]
@@ -1,5 +1,7 @@
1
1
  """Library-side combat resolver subset of engine_dispatch.
2
2
 
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+
3
5
  Hosts the pure rules-engine resolvers for the four combat-flavored
4
6
  action types: ATTACK, CAST_SPELL, SKILL_CHECK, SAVING_THROW. Host
5
7
  callers (e.g. a host's the host)
@@ -352,3 +354,14 @@ __all__ = [
352
354
  "DispatchContext",
353
355
  "resolve_combat_action",
354
356
  ]
357
+
358
+
359
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
360
+ import warnings as _warnings # noqa: E402
361
+
362
+ _warnings.warn(
363
+ "dnd5e_engine.dispatch is part of the legacy (Gen 1) surface and will be "
364
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
365
+ DeprecationWarning,
366
+ stacklevel=2,
367
+ )
@@ -1,5 +1,7 @@
1
1
  """Pure CombatEvent → dict serialization.
2
2
 
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+
3
5
  Used by the host's WS-envelope adapter in the host.
4
6
  Library-internal only — no WS protocol concerns leak in here. The host
5
7
  owns host-specific envelope wrapping (SystemMessage, CombatEnd,
@@ -35,3 +37,14 @@ def events_to_dicts(events: Iterable[CombatEvent]) -> list[dict[str, Any]]:
35
37
 
36
38
 
37
39
  __all__ = ["event_to_dict", "events_to_dicts"]
40
+
41
+
42
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
43
+ import warnings as _warnings # noqa: E402
44
+
45
+ _warnings.warn(
46
+ "dnd5e_engine.event_dicts is part of the legacy (Gen 1) surface and will be "
47
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
48
+ DeprecationWarning,
49
+ stacklevel=2,
50
+ )
@@ -119,7 +119,6 @@ from dnd5e_engine.rules.conditions import (
119
119
  project_passive_damage_modifiers,
120
120
  project_passive_save_modifiers,
121
121
  )
122
- from dnd5e_engine.rules.gambits import BehaviorProfile
123
122
  from dnd5e_engine.spatial import GridTopology, SpatialTopology
124
123
  from dnd5e_engine.specs import (
125
124
  EncounterMemberSpec,
@@ -128,7 +127,7 @@ from dnd5e_engine.specs import (
128
127
  SceneTopology,
129
128
  ZoneEdge,
130
129
  )
131
- from dnd5e_engine.types.combat import Combatant
130
+ from dnd5e_engine.types.combat import BehaviorProfile, Combatant
132
131
  from dnd5e_engine.types.conditions import ActiveCondition
133
132
  from dnd5e_engine.types.effects import ActiveEffect, ActiveEffectDuration
134
133
  from dnd5e_engine.views import LiveCombatView
@@ -1,4 +1,7 @@
1
- """Combat resolution — attack rolls, damage, saving throws."""
1
+ """Combat resolution — attack rolls, damage, saving throws.
2
+
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+ """
2
5
 
3
6
  from __future__ import annotations
4
7
 
@@ -579,3 +582,14 @@ __all__ = [
579
582
  "resolve_player_attack",
580
583
  "saving_throw",
581
584
  ]
585
+
586
+
587
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
588
+ import warnings as _warnings # noqa: E402
589
+
590
+ _warnings.warn(
591
+ "dnd5e_engine.rules.combat is part of the legacy (Gen 1) surface and will be "
592
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
593
+ DeprecationWarning,
594
+ stacklevel=2,
595
+ )
@@ -1,5 +1,7 @@
1
1
  """Combat data helpers — spell scaling, modifiers, weapon stats.
2
2
 
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+
3
5
  Pure functions with zero DB imports. Used by the combat engine to calculate
4
6
  derived stats from structured spell and weapon data.
5
7
  """
@@ -228,3 +230,14 @@ __all__ = [
228
230
  "unarmed_strike_damage",
229
231
  "weapon_attack_bonus",
230
232
  ]
233
+
234
+
235
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
236
+ import warnings as _warnings # noqa: E402
237
+
238
+ _warnings.warn(
239
+ "dnd5e_engine.rules.combat_data is part of the legacy (Gen 1) surface and will be "
240
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
241
+ DeprecationWarning,
242
+ stacklevel=2,
243
+ )
@@ -1,5 +1,7 @@
1
1
  """Combat utility helpers — initiative sorting, death save state, health descriptors.
2
2
 
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+
3
5
  Zero DB imports. All functions are pure or use only dnd5e_engine.rules.* imports.
4
6
  """
5
7
 
@@ -7,9 +9,9 @@ from __future__ import annotations
7
9
 
8
10
  import json
9
11
  import random
10
- from dataclasses import dataclass
11
12
  from typing import Any
12
13
 
14
+ from dnd5e_engine.death_saves import DeathSaveState # re-export; canonical home moved in 0.4.0
13
15
  from dnd5e_engine.rules.combat import initiative_roll
14
16
  from dnd5e_engine.rules.dice import ability_modifier
15
17
  from dnd5e_engine.types.combat import CombatNPC
@@ -200,95 +202,6 @@ def roll_and_sort_initiative(combatants: list[dict[str, Any]]) -> list[dict[str,
200
202
  # ---------------------------------------------------------------------------
201
203
 
202
204
 
203
- @dataclass
204
- class DeathSaveState:
205
- """Mutable state machine for D&D 5e death saving throws.
206
-
207
- Designed for host persistence via to_dict / from_dict.
208
- """
209
-
210
- successes: int = 0
211
- failures: int = 0
212
- is_stable: bool = False
213
-
214
- # ------------------------------------------------------------------
215
- # Core state transitions
216
- # ------------------------------------------------------------------
217
-
218
- def apply_save(self, success: bool, is_critical: bool) -> str:
219
- """Apply a death saving throw result.
220
-
221
- Returns one of: "critical_success" | "stabilized" | "dead" | "ongoing".
222
-
223
- Rules:
224
- - Nat 20 (success=True, is_critical=True): regain 1 HP -> "critical_success"
225
- - Nat 1 (success=False, is_critical=True): 2 failures
226
- - Normal success: 1 success; 3 successes -> "stabilized"
227
- - Normal failure: 1 failure; 3 failures -> "dead"
228
- """
229
- # Nat 20 takes priority — regain 1 HP, no counter update needed
230
- if success and is_critical:
231
- return "critical_success"
232
-
233
- if success:
234
- self.successes += 1
235
- elif is_critical:
236
- # Nat 1 counts as 2 failures
237
- self.failures += 2
238
- else:
239
- self.failures += 1
240
-
241
- return self._check_outcome()
242
-
243
- def apply_damage_while_unconscious(self, is_melee_within_5ft: bool) -> str:
244
- """Apply death save failures from damage while unconscious.
245
-
246
- D&D 5e RAW: taking any damage while at 0 HP is a death save failure.
247
- Melee attack within 5 ft = auto-crit = 2 failures. Otherwise 1 failure.
248
-
249
- Returns "dead" if 3+ failures reached, else "ongoing".
250
- """
251
- self.failures += 2 if is_melee_within_5ft else 1
252
- return self._check_outcome()
253
-
254
- def reset(self) -> None:
255
- """Reset all counters (called when character regains HP via nat 20)."""
256
- self.successes = 0
257
- self.failures = 0
258
- self.is_stable = False
259
-
260
- # ------------------------------------------------------------------
261
- # Serialization (plain dicts for host persistence)
262
- # ------------------------------------------------------------------
263
-
264
- def to_dict(self) -> dict[str, Any]:
265
- return {
266
- "successes": self.successes,
267
- "failures": self.failures,
268
- "is_stable": self.is_stable,
269
- }
270
-
271
- @classmethod
272
- def from_dict(cls, d: dict[str, Any]) -> DeathSaveState:
273
- return cls(
274
- successes=d.get("successes", 0),
275
- failures=d.get("failures", 0),
276
- is_stable=d.get("is_stable", False),
277
- )
278
-
279
- # ------------------------------------------------------------------
280
- # Internal helpers
281
- # ------------------------------------------------------------------
282
-
283
- def _check_outcome(self) -> str:
284
- if self.failures >= 3:
285
- return "dead"
286
- if self.successes >= 3:
287
- self.is_stable = True
288
- return "stabilized"
289
- return "ongoing"
290
-
291
-
292
205
  def build_combatant_from_npc(
293
206
  npc_props: dict[str, Any],
294
207
  template_props: dict[str, Any],
@@ -412,3 +325,14 @@ __all__ = [
412
325
  "health_descriptor",
413
326
  "roll_and_sort_initiative",
414
327
  ]
328
+
329
+
330
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
331
+ import warnings as _warnings # noqa: E402
332
+
333
+ _warnings.warn(
334
+ "dnd5e_engine.rules.combat_helpers is part of the legacy (Gen 1) surface and will be "
335
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
336
+ DeprecationWarning,
337
+ stacklevel=2,
338
+ )
@@ -1,5 +1,7 @@
1
1
  """Equipment rules — AC calculation and armor proficiency checks.
2
2
 
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+
3
5
  Pure functions with zero DB imports. Used by the combat engine to calculate
4
6
  AC from character equipment data fetched by build_dispatch_context().
5
7
 
@@ -106,3 +108,14 @@ __all__ = [
106
108
  "calculate_ac",
107
109
  "is_armor_proficient",
108
110
  ]
111
+
112
+
113
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
114
+ import warnings as _warnings # noqa: E402
115
+
116
+ _warnings.warn(
117
+ "dnd5e_engine.rules.equipment is part of the legacy (Gen 1) surface and will be "
118
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
119
+ DeprecationWarning,
120
+ stacklevel=2,
121
+ )
@@ -1,5 +1,7 @@
1
1
  """Deterministic monster AI gambit system.
2
2
 
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+
3
5
  Selects actions for monsters based on behavior profile and current game state.
4
6
  Zero DB imports. Uses only dnd5e_engine.rules.* imports.
5
7
  """
@@ -8,22 +10,16 @@ from __future__ import annotations
8
10
 
9
11
  import re
10
12
  from dataclasses import dataclass
11
- from enum import StrEnum
12
13
  from typing import Any
13
14
 
14
15
  from dnd5e_engine.rules.combat import HitType, attack_roll, damage_roll
16
+ from dnd5e_engine.types.combat import BehaviorProfile # re-export; canonical home moved in 0.4.0
15
17
 
16
18
  # ---------------------------------------------------------------------------
17
19
  # Enums and dataclasses
18
20
  # ---------------------------------------------------------------------------
19
21
 
20
22
 
21
- class BehaviorProfile(StrEnum):
22
- AGGRESSIVE = "AGGRESSIVE"
23
- RANGED = "RANGED"
24
- DEFENSIVE = "DEFENSIVE"
25
-
26
-
27
23
  @dataclass(frozen=True)
28
24
  class GambitAction:
29
25
  """An action selected by the gambit system for a monster."""
@@ -184,3 +180,14 @@ __all__ = [
184
180
  "parse_damage_dice",
185
181
  "resolve_monster_action",
186
182
  ]
183
+
184
+
185
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
186
+ import warnings as _warnings # noqa: E402
187
+
188
+ _warnings.warn(
189
+ "dnd5e_engine.rules.gambits is part of the legacy (Gen 1) surface and will be "
190
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
191
+ DeprecationWarning,
192
+ stacklevel=2,
193
+ )
@@ -1,4 +1,6 @@
1
1
  """
2
+
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
2
4
  Dice resolution helpers used by the v2.2 engine dispatch layer.
3
5
 
4
6
  Pure module: zero imports from neo4j, asyncpg, redis, or the host (Neo4j/PG/host storage layers).
@@ -165,3 +167,14 @@ __all__ = [
165
167
  "extract_skill_from_roll_type",
166
168
  "parse_ability_scores",
167
169
  ]
170
+
171
+
172
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
173
+ import warnings as _warnings # noqa: E402
174
+
175
+ _warnings.warn(
176
+ "dnd5e_engine.rules.resolution is part of the legacy (Gen 1) surface and will be "
177
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
178
+ DeprecationWarning,
179
+ stacklevel=2,
180
+ )
@@ -1,4 +1,7 @@
1
- """Spell slot management and spell resolution helpers."""
1
+ """Spell slot management and spell resolution helpers.
2
+
3
+ **DEPRECATED (0.4.0) — removed in 0.5.0.** See docs/migration/v0.3-to-v0.4.md.
4
+ """
2
5
 
3
6
  from __future__ import annotations
4
7
 
@@ -149,3 +152,14 @@ __all__ = [
149
152
  "spell_slots_for_class",
150
153
  "upcast_bonus_dice",
151
154
  ]
155
+
156
+
157
+ # ── 0.4.0 deprecation (module-level, fires once on first import) ─────────────
158
+ import warnings as _warnings # noqa: E402
159
+
160
+ _warnings.warn(
161
+ "dnd5e_engine.rules.spells is part of the legacy (Gen 1) surface and will be "
162
+ "removed in dnd5e-engine 0.5.0 — see docs/migration/v0.3-to-v0.4.md.",
163
+ DeprecationWarning,
164
+ stacklevel=2,
165
+ )
@@ -0,0 +1,49 @@
1
+ """dnd5e-engine type surface — host-agnostic Pydantic + Literal types."""
2
+
3
+ from dnd5e_engine.types.combat import Combatant
4
+ from dnd5e_engine.types.conditions import ActiveCondition, ConditionScope
5
+ from dnd5e_engine.types.effects import (
6
+ ActiveEffect,
7
+ ActiveEffectChange,
8
+ ActiveEffectDuration,
9
+ )
10
+
11
+ __all__ = [
12
+ "ActionType",
13
+ "ActiveCondition",
14
+ "ActiveEffect",
15
+ "ActiveEffectChange",
16
+ "ActiveEffectDuration",
17
+ "CombatNPC",
18
+ "CombatOutcome",
19
+ "Combatant",
20
+ "ConditionScope",
21
+ "DiceOutcome",
22
+ ]
23
+
24
+ _GEN1_LAZY = {
25
+ "ActionType": ("dnd5e_engine.types.intent", "ActionType"),
26
+ "CombatOutcome": ("dnd5e_engine.types.intent", "CombatOutcome"),
27
+ "DiceOutcome": ("dnd5e_engine.types.dice", "DiceOutcome"),
28
+ "CombatNPC": ("dnd5e_engine.types.combat", "CombatNPC"),
29
+ }
30
+
31
+
32
+ def __getattr__(name: str) -> object:
33
+ # PEP 562 — keep the 0.3.x names resolvable without importing the
34
+ # deprecated modules eagerly (which would warn on every ``import dnd5e_engine.types``).
35
+ if name in _GEN1_LAZY:
36
+ import importlib
37
+ import warnings
38
+
39
+ module, attr = _GEN1_LAZY[name]
40
+ warnings.warn(
41
+ f"dnd5e_engine.types.{name} belongs to the legacy (Gen 1) surface — a host-side "
42
+ f"shape, not an engine type — and will be removed in dnd5e-engine 0.5.0. "
43
+ f"Import it from {module} (also deprecated) or copy it into your host; "
44
+ f"see docs/migration/v0.3-to-v0.4.md.",
45
+ DeprecationWarning,
46
+ stacklevel=2,
47
+ )
48
+ return getattr(importlib.import_module(module), attr)
49
+ raise AttributeError(f"module 'dnd5e_engine.types' has no attribute {name!r}")
@@ -7,6 +7,7 @@ state; hosts read it through ``dnd5e_engine.views.LiveCombatView``.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from enum import StrEnum
10
11
  from typing import Any
11
12
 
12
13
  from pydantic import BaseModel, Field, model_validator
@@ -15,6 +16,14 @@ from dnd5e_engine.activities.passive_stats import CombatantMovementModes, Combat
15
16
  from dnd5e_engine.types.conditions import ActiveCondition
16
17
 
17
18
 
19
+ class BehaviorProfile(StrEnum):
20
+ """Monster AI posture consumed by the orchestrator's flee heuristic."""
21
+
22
+ AGGRESSIVE = "AGGRESSIVE"
23
+ RANGED = "RANGED"
24
+ DEFENSIVE = "DEFENSIVE"
25
+
26
+
18
27
  class CombatNPC(BaseModel):
19
28
  """Ephemeral sidecar record for an NPC in combat. Host-scoped;
20
29
  cleared at end of combat. Owns template-derived combat stats that don't
@@ -214,6 +223,7 @@ class Combatant(BaseModel):
214
223
 
215
224
 
216
225
  __all__ = [
226
+ "BehaviorProfile",
217
227
  "CombatNPC",
218
228
  "Combatant",
219
229
  ]