dnd5e-engine 0.4.0__tar.gz → 0.5.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 (165) hide show
  1. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/CHANGELOG.md +19 -0
  2. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/PKG-INFO +1 -1
  3. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/pyproject.toml +2 -4
  4. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/__init__.py +0 -23
  5. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/specs.py +1 -1
  6. dnd5e_engine-0.5.0/src/dnd5e_engine/types/__init__.py +18 -0
  7. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/types/combat.py +2 -32
  8. dnd5e_engine-0.5.0/tests/activities/test_save_dc_and_damage_exprs.py +129 -0
  9. dnd5e_engine-0.5.0/tests/test_attack_classification.py +84 -0
  10. dnd5e_engine-0.5.0/tests/test_combat_bucket_keys.py +61 -0
  11. dnd5e_engine-0.5.0/tests/test_combat_flag_advantage.py +25 -0
  12. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_gen2_has_no_gen1_imports.py +6 -0
  13. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_public_api_surface.py +0 -12
  14. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_combat_data_and_skills.py +0 -149
  15. dnd5e_engine-0.5.0/tests/test_rules_dice_equipment_spells.py +302 -0
  16. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_effects_and_gambits.py +0 -133
  17. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_smoke.py +1 -8
  18. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_types_smoke.py +0 -73
  19. dnd5e_engine-0.4.0/src/dnd5e_engine/dispatch.py +0 -367
  20. dnd5e_engine-0.4.0/src/dnd5e_engine/event_dicts.py +0 -50
  21. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/_class_meta.py +0 -14
  22. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/_parsing.py +0 -36
  23. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat.py +0 -595
  24. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat_data.py +0 -243
  25. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat_helpers.py +0 -338
  26. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/equipment.py +0 -121
  27. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/gambits.py +0 -193
  28. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/resolution.py +0 -180
  29. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/spells.py +0 -165
  30. dnd5e_engine-0.4.0/src/dnd5e_engine/types/__init__.py +0 -49
  31. dnd5e_engine-0.4.0/src/dnd5e_engine/types/dice.py +0 -46
  32. dnd5e_engine-0.4.0/src/dnd5e_engine/types/intent.py +0 -187
  33. dnd5e_engine-0.4.0/tests/test_combat_bucket_keys.py +0 -177
  34. dnd5e_engine-0.4.0/tests/test_combat_flag_advantage.py +0 -154
  35. dnd5e_engine-0.4.0/tests/test_gen1_deprecation_warnings.py +0 -63
  36. dnd5e_engine-0.4.0/tests/test_generation_parity_attack.py +0 -179
  37. dnd5e_engine-0.4.0/tests/test_rules_combat_helpers.py +0 -654
  38. dnd5e_engine-0.4.0/tests/test_rules_combat_resolution.py +0 -802
  39. dnd5e_engine-0.4.0/tests/test_rules_dice_equipment_spells.py +0 -563
  40. dnd5e_engine-0.4.0/tests/test_rules_resolution_and_parsing.py +0 -368
  41. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/.gitignore +0 -0
  42. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/LICENSE +0 -0
  43. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/Makefile +0 -0
  44. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/NOTICE +0 -0
  45. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/README.md +0 -0
  46. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/scripts/_smoke_grid_combat.py +0 -0
  47. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/scripts/smoke_clean_install.sh +0 -0
  48. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/__init__.py +0 -0
  49. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/apply.py +0 -0
  50. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/attack.py +0 -0
  51. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/build_context.py +0 -0
  52. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/cast.py +0 -0
  53. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/check.py +0 -0
  54. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/context.py +0 -0
  55. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/damage.py +0 -0
  56. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/dice.py +0 -0
  57. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/effects.py +0 -0
  58. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/formula.py +0 -0
  59. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/heal.py +0 -0
  60. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/mastery.py +0 -0
  61. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/monster_actions.py +0 -0
  62. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/passive_stats.py +0 -0
  63. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/resolver.py +0 -0
  64. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/save.py +0 -0
  65. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/save_primitive.py +0 -0
  66. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/scale.py +0 -0
  67. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/build_party.py +0 -0
  68. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/build_spec.py +0 -0
  69. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/check.py +0 -0
  70. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/death_saves.py +0 -0
  71. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/events.py +0 -0
  72. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/lib_loader.py +0 -0
  73. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/orchestrator.py +0 -0
  74. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/outcome.py +0 -0
  75. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/py.typed +0 -0
  76. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rest.py +0 -0
  77. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/results.py +0 -0
  78. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/__init__.py +0 -0
  79. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/conditions.py +0 -0
  80. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/dice.py +0 -0
  81. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/effects.py +0 -0
  82. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/skills.py +0 -0
  83. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/spatial.py +0 -0
  84. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/testing.py +0 -0
  85. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/types/conditions.py +0 -0
  86. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/types/effects.py +0 -0
  87. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/views.py +0 -0
  88. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/__init__.py +0 -0
  89. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/__init__.py +0 -0
  90. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_build_context.py +0 -0
  91. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_cast_level_override.py +0 -0
  92. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_monster_actions.py +0 -0
  93. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_multiattack_fidelity.py +0 -0
  94. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_resolve_check_activity.py +0 -0
  95. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_save_primitive.py +0 -0
  96. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_weapon_mastery.py +0 -0
  97. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/__init__.py +0 -0
  98. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/conftest.py +0 -0
  99. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/harness.py +0 -0
  100. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
  101. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
  102. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
  103. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c03_effect_modes.py +0 -0
  104. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
  105. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c05_spatial.py +0 -0
  106. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c06_reactions.py +0 -0
  107. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c07_sneak_and_repertoire.py +0 -0
  108. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c08_passive_projection.py +0 -0
  109. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c09_rest.py +0 -0
  110. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
  111. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c11_item_charges.py +0 -0
  112. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_active_effect_resistance_fold.py +0 -0
  113. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_active_effect_schema.py +0 -0
  114. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_apply_changes_to_check.py +0 -0
  115. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_apply_modifiers.py +0 -0
  116. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_build_context_abilities.py +0 -0
  117. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_build_party.py +0 -0
  118. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_build_party_passives.py +0 -0
  119. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_capability_matrix.py +0 -0
  120. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_concentration_drop_effect_name.py +0 -0
  121. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_concentration_repeat_save_identity.py +0 -0
  122. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_cover_bonus_sidecar.py +0 -0
  123. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_coverage_backfill_death_saves.py +0 -0
  124. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
  125. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_determinism_contract.py +0 -0
  126. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_docstrings_are_host_agnostic.py +0 -0
  127. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_duration_tick_at_turn_end.py +0 -0
  128. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_effects_builder_internals.py +0 -0
  129. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_enchantment_sidecar_projection.py +0 -0
  130. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_end_combat_snapshot.py +0 -0
  131. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_expire_clears_active_conditions.py +0 -0
  132. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
  133. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_get_actor_active_effects.py +0 -0
  134. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_grid_topology.py +0 -0
  135. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_iter11_fixes.py +0 -0
  136. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_iter13_fixes.py +0 -0
  137. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_iter5_fixes.py +0 -0
  138. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_lib_loader.py +0 -0
  139. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_lib_loader_configure.py +0 -0
  140. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_live_combat_view.py +0 -0
  141. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_monster_dash.py +0 -0
  142. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_monster_flee_retreat.py +0 -0
  143. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_gating_typed.py +0 -0
  144. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_grid_combat.py +0 -0
  145. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_item_charges.py +0 -0
  146. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_monster_typed.py +0 -0
  147. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
  148. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_pc_resolution_typed.py +0 -0
  149. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_party_member_reach_ft.py +0 -0
  150. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_passive_stats.py +0 -0
  151. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_phase6_public_surface.py +0 -0
  152. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rage_second_wind_e2e.py +0 -0
  153. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_reaction_queue.py +0 -0
  154. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_resolve_check.py +0 -0
  155. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_resolve_check_flag_advantage.py +0 -0
  156. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_resolve_check_specific_buckets.py +0 -0
  157. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rest.py +0 -0
  158. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_conditions.py +0 -0
  159. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_scale_resolver.py +0 -0
  160. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_smoke.py +0 -0
  161. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_sneak_attack_resolver.py +0 -0
  162. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_start_combat_active_effects.py +0 -0
  163. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
  164. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_use_feature_intent.py +0 -0
  165. {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0]
11
+
12
+ Lockstep release with `dnd5e-srd-data` 0.5.0 and `nat20-bridge` 0.5.0.
13
+
14
+ Removes the legacy (Gen 1) rules surface deprecated in 0.4.0. The engine now
15
+ ships exactly one, seedable, documented rules implementation
16
+ (`orchestrator` + `activities/`). See `docs/migration/v0.4-to-v0.5.md`.
17
+
18
+ ### Removed
19
+
20
+ - Modules `dispatch`, `event_dicts`, `types.dice`, `types.intent`, and
21
+ `rules.{combat, combat_data, combat_helpers, equipment, gambits, resolution,
22
+ spells}` (plus the private `rules._parsing` / `rules._class_meta`).
23
+ - `dnd5e_engine.ActionType`, `CombatNPC`, and the lazy
24
+ `dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
25
+ re-exports; the 0.4.0 `DeathSaveState` / `BehaviorProfile` re-export shims.
26
+ - The cross-generation parity test; its Gen 2 assertions live on in
27
+ `tests/test_attack_classification.py`.
28
+
10
29
  ## [0.4.0]
11
30
 
12
31
  Lockstep release with `dnd5e-srd-data` 0.4.0 and `nat20-bridge` 0.4.0.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dnd5e-engine
3
- Version: 0.4.0
3
+ Version: 0.5.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.4.0"
7
+ version = "0.5.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"
@@ -88,10 +88,8 @@ max-complexity = 23
88
88
  [tool.pytest.ini_options]
89
89
  testpaths = ["tests"]
90
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.
91
+ # Any DeprecationWarning the engine itself raises is a bug.
93
92
  "error::DeprecationWarning:dnd5e_engine",
94
- "ignore:.*legacy \\(Gen 1\\) surface.*:DeprecationWarning",
95
93
  ]
96
94
 
97
95
  [tool.coverage.run]
@@ -82,7 +82,6 @@ from dnd5e_engine.types.effects import (
82
82
 
83
83
  __all__ = [
84
84
  "AbilityScores",
85
- "ActionType",
86
85
  "ActiveEffect",
87
86
  "ActiveEffectChange",
88
87
  "ActiveEffectDuration",
@@ -129,25 +128,3 @@ __all__ = [
129
128
  "start_combat",
130
129
  "submit_player_intent",
131
130
  ]
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}")
@@ -170,7 +170,7 @@ class EncounterMemberSpec(BaseModel):
170
170
  # Person targets humanoids; Sleep autopasses undead/elves; etc.).
171
171
  creature_type: str | None = None
172
172
  # SRD §Damage Resistance / §Damage Immunity — per-monster type lists,
173
- # populated from MonsterTemplate (via CombatMonster / CombatNPC). Empty
173
+ # populated from the host's monster template record. Empty
174
174
  # by default for fixtures that don't specify.
175
175
  damage_resistances: list[str] = Field(default_factory=list)
176
176
  damage_immunities: list[str] = Field(default_factory=list)
@@ -0,0 +1,18 @@
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
+ "ActiveCondition",
13
+ "ActiveEffect",
14
+ "ActiveEffectChange",
15
+ "ActiveEffectDuration",
16
+ "Combatant",
17
+ "ConditionScope",
18
+ ]
@@ -1,4 +1,4 @@
1
- """Combat participant types — ``Combatant`` + ``CombatNPC``.
1
+ """Combat participant types — ``Combatant`` and ``BehaviorProfile``.
2
2
 
3
3
  Host-agnostic value types: stdlib + pydantic + the engine's own
4
4
  ``ActiveCondition``. ``Combatant`` is the engine's per-creature runtime combat
@@ -24,35 +24,6 @@ class BehaviorProfile(StrEnum):
24
24
  DEFENSIVE = "DEFENSIVE"
25
25
 
26
26
 
27
- class CombatNPC(BaseModel):
28
- """Ephemeral sidecar record for an NPC in combat. Host-scoped;
29
- cleared at end of combat. Owns template-derived combat stats that don't
30
- fit on the narrow Combatant: saves, resistances, immunities, ability
31
- scores, behavior_profile. Symmetric to CombatMonster for non-Character
32
- entities."""
33
-
34
- npc_id: str # persistent NPC node ID (npc:hex12)
35
- template_id: str # MonsterTemplate node ID used at materialization
36
- name: str
37
- hp_current: int
38
- hp_max: int
39
- ac: int
40
- attack_bonus: int
41
- damage_dice: str
42
- damage_type: str
43
- has_ranged_attack: bool = False
44
- dexterity: int = 10
45
- strength: int = 10
46
- constitution: int = 10
47
- wisdom: int = 10
48
- intelligence: int = 10
49
- charisma: int = 10
50
- behavior_profile: str = "AGGRESSIVE"
51
- damage_resistances: list[str] = Field(default_factory=list)
52
- damage_immunities: list[str] = Field(default_factory=list)
53
- is_alive: bool = True
54
-
55
-
56
27
  class Combatant(BaseModel):
57
28
  entity_id: str
58
29
  entity_type: str # "Character" | "Monster" | "NPC"
@@ -110,7 +81,7 @@ class Combatant(BaseModel):
110
81
  # application path (``activities/effects.py::apply_activity_effects``)
111
82
  # suppresses a ``ConditionApplied`` whose condition is in this list
112
83
  # . Empty by default. NOTE: distinct from the dead, host-supplied
113
- # ``dispatch.py::DispatchContext.condition_immunities`` legacy surface.
84
+ # legacy dispatch surface's ``condition_immunities`` (removed in 0.5.0).
114
85
  condition_immunities: list[str] = Field(default_factory=list)
115
86
  # SRD §Senses — special senses in feet (darkvision/blindsight/tremorsense/
116
87
  # truesight). Projected from PC species + always-on feature passive_effects
@@ -224,6 +195,5 @@ class Combatant(BaseModel):
224
195
 
225
196
  __all__ = [
226
197
  "BehaviorProfile",
227
- "CombatNPC",
228
198
  "Combatant",
229
199
  ]
@@ -0,0 +1,129 @@
1
+ """Direct pins on ``activities/save.py::_resolve_dc`` and the damage-expression
2
+ builders in ``activities/dice.py`` / ``activities/formula.py``.
3
+
4
+ The end-to-end scenarios reach these only through the corpus' common shapes
5
+ (spellcasting DC, automatic ``{n}d{d}`` formulas); the flat-DC, custom-formula
6
+ and scaling-token branches are pinned here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import random
12
+
13
+ import pytest
14
+ from dnd5e_srd_data.loader import BundledAssetLoader
15
+ from dnd5e_srd_data.schema.common import (
16
+ DamageCustomBlock,
17
+ DamagePart,
18
+ DamagePartBlock,
19
+ DamageScalingBlock,
20
+ SaveActivity,
21
+ )
22
+
23
+ from dnd5e_engine.activities.context import ActivityResolutionContext
24
+ from dnd5e_engine.activities.dice import damage_part_to_expr
25
+ from dnd5e_engine.activities.formula import resolve_damage_block
26
+ from dnd5e_engine.activities.save import _resolve_dc
27
+ from dnd5e_engine.types.combat import Combatant
28
+
29
+
30
+ def _ctx(**overrides: object) -> ActivityResolutionContext:
31
+ kwargs: dict[str, object] = dict(
32
+ rng=random.Random(1),
33
+ caster=Combatant(
34
+ entity_id="char:wiz",
35
+ entity_type="Character",
36
+ name="Wiz",
37
+ initiative=10,
38
+ hp_current=10,
39
+ hp_max=10,
40
+ ),
41
+ targets=[],
42
+ event_emitter=lambda ev: None,
43
+ caster_abilities={"str": 10, "dex": 10, "con": 10, "int": 16, "wis": 10, "cha": 10},
44
+ caster_proficiency_bonus=2,
45
+ spellcasting_ability="int",
46
+ )
47
+ kwargs.update(overrides)
48
+ return ActivityResolutionContext(**kwargs) # type: ignore[arg-type]
49
+
50
+
51
+ @pytest.fixture(scope="module")
52
+ def fireball_save() -> SaveActivity:
53
+ spell = BundledAssetLoader().get_spell("fireball")
54
+ assert spell is not None
55
+ activity = next(a for a in spell.activities if a.kind == "save")
56
+ assert isinstance(activity, SaveActivity)
57
+ return activity
58
+
59
+
60
+ def _with_dc(activity: SaveActivity, calculation: str, formula: str = "") -> SaveActivity:
61
+ dc = activity.save.dc.model_copy(update={"calculation": calculation, "formula": formula})
62
+ return activity.model_copy(update={"save": activity.save.model_copy(update={"dc": dc})})
63
+
64
+
65
+ # ── _resolve_dc ──────────────────────────────────────────────────────────────
66
+
67
+
68
+ def test_spellcasting_dc_is_8_plus_prof_plus_mod(fireball_save: SaveActivity) -> None:
69
+ assert _resolve_dc(fireball_save, _ctx()) == 8 + 2 + 3
70
+
71
+
72
+ def test_spellcasting_dc_without_a_casting_ability_is_loud(fireball_save: SaveActivity) -> None:
73
+ with pytest.raises(ValueError, match="spellcasting ability"):
74
+ _resolve_dc(fireball_save, _ctx(spellcasting_ability=None))
75
+
76
+
77
+ def test_flat_dc_resolves_the_formula(fireball_save: SaveActivity) -> None:
78
+ assert _resolve_dc(_with_dc(fireball_save, "flat", "13"), _ctx()) == 13
79
+
80
+
81
+ def test_flat_dc_resolves_roll_data_tokens(fireball_save: SaveActivity) -> None:
82
+ # @prof folds to the caster's proficiency bonus before evaluation.
83
+ assert _resolve_dc(_with_dc(fireball_save, "flat", "10 + @prof"), _ctx()) == 12
84
+
85
+
86
+ def test_unknown_dc_calculation_is_loud(fireball_save: SaveActivity) -> None:
87
+ with pytest.raises(ValueError, match="not resolvable"):
88
+ _resolve_dc(_with_dc(fireball_save, "mystery"), _ctx())
89
+
90
+
91
+ def test_save_dc_override_bypasses_the_calculation(fireball_save: SaveActivity) -> None:
92
+ assert _resolve_dc(_with_dc(fireball_save, "mystery"), _ctx(save_dc_override=18)) == 18
93
+
94
+
95
+ # ── damage_part_to_expr ──────────────────────────────────────────────────────
96
+
97
+
98
+ def test_weapon_damage_part_passes_its_dice_string_through() -> None:
99
+ assert damage_part_to_expr(DamagePart(dice="2d6+3", damage_type="slashing")) == "2d6+3"
100
+
101
+
102
+ def test_activity_block_builds_the_automatic_formula() -> None:
103
+ assert damage_part_to_expr(DamagePartBlock(number=2, denomination=6, bonus="3")) == "2d6+3"
104
+
105
+
106
+ def test_activity_block_with_only_a_bonus_is_the_bare_bonus() -> None:
107
+ assert damage_part_to_expr(DamagePartBlock(bonus="5")) == "5"
108
+
109
+
110
+ def test_activity_block_custom_formula_wins_when_enabled() -> None:
111
+ block = DamagePartBlock(
112
+ number=2, denomination=6, custom=DamageCustomBlock(enabled=True, formula="1d4")
113
+ )
114
+ assert damage_part_to_expr(block) == "1d4"
115
+
116
+
117
+ # ── resolve_damage_block ─────────────────────────────────────────────────────
118
+
119
+
120
+ def test_scaling_formula_tokens_are_resolved_off_the_context() -> None:
121
+ block = DamagePartBlock(number=1, denomination=8, scaling=DamageScalingBlock(formula="@prof"))
122
+ resolved = resolve_damage_block(block, _ctx(), ability="int")
123
+ assert resolved.scaling.formula == "2"
124
+ assert resolved.number == 1 # untouched fields survive the copy
125
+
126
+
127
+ def test_block_without_tokens_is_returned_unchanged() -> None:
128
+ block = DamagePartBlock(number=1, denomination=8, bonus="2")
129
+ assert resolve_damage_block(block, _ctx(), ability="int") is block
@@ -0,0 +1,84 @@
1
+ """Pin the SRD hit / crit / miss classification of the attack resolver.
2
+
3
+ Carried over from the (now retired) cross-generation parity test: these are
4
+ the rules whose drift between the two former implementations motivated the
5
+ 0.5.0 consolidation, so they stay pinned on the surviving path.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import random
11
+
12
+ import pytest
13
+ from dnd5e_srd_data.loader import BundledAssetLoader
14
+ from dnd5e_srd_data.schema.common import AttackActivity
15
+
16
+ from dnd5e_engine.activities.attack import _resolve_hit_outcome, _roll_natural_d20
17
+ from dnd5e_engine.activities.context import ActivityResolutionContext
18
+ from dnd5e_engine.types.combat import Combatant
19
+
20
+
21
+ @pytest.fixture(scope="module")
22
+ def attack() -> AttackActivity:
23
+ longsword = BundledAssetLoader().get_weapon("longsword")
24
+ assert longsword is not None
25
+ activity = next(a for a in longsword.activities if a.kind == "attack")
26
+ assert isinstance(activity, AttackActivity)
27
+ return activity
28
+
29
+
30
+ def _ctx(seed: int) -> ActivityResolutionContext:
31
+ hero = Combatant(
32
+ entity_id="char:hero",
33
+ entity_type="Character",
34
+ name="Hero",
35
+ initiative=10,
36
+ hp_current=10,
37
+ hp_max=10,
38
+ )
39
+ return ActivityResolutionContext(
40
+ rng=random.Random(seed),
41
+ caster=hero,
42
+ targets=[],
43
+ event_emitter=lambda ev: None,
44
+ caster_abilities={"str": 10, "dex": 10, "con": 10, "int": 10, "wis": 10, "cha": 10},
45
+ )
46
+
47
+
48
+ def test_natural_one_always_misses_even_with_a_huge_bonus(attack: AttackActivity) -> None:
49
+ assert _resolve_hit_outcome(1, 1 + 50, 10, attack) == (False, False)
50
+
51
+
52
+ def test_natural_twenty_always_crits_even_against_impossible_ac(attack: AttackActivity) -> None:
53
+ assert _resolve_hit_outcome(20, 20, 99, attack) == (True, True)
54
+
55
+
56
+ def test_meeting_the_ac_exactly_is_a_hit(attack: AttackActivity) -> None:
57
+ assert _resolve_hit_outcome(10, 15, 15, attack) == (False, True)
58
+
59
+
60
+ def test_falling_one_short_of_the_ac_is_a_miss(attack: AttackActivity) -> None:
61
+ assert _resolve_hit_outcome(10, 14, 15, attack) == (False, False)
62
+
63
+
64
+ @pytest.mark.parametrize("seed", range(32))
65
+ def test_advantage_keeps_the_higher_and_disadvantage_the_lower_die(seed: int) -> None:
66
+ rng = random.Random(seed)
67
+ a, b = rng.randint(1, 20), rng.randint(1, 20)
68
+ assert _roll_natural_d20(_ctx(seed), "advantage") == max(a, b)
69
+ assert _roll_natural_d20(_ctx(seed), "disadvantage") == min(a, b)
70
+ assert _roll_natural_d20(_ctx(seed), "normal") == a
71
+
72
+
73
+ def test_disadvantage_does_not_crit_on_the_discarded_die(attack: AttackActivity) -> None:
74
+ # Find a seed whose two dice are (20, x<20): the kept die under disadvantage
75
+ # must not be the discarded 20.
76
+ for seed in range(500):
77
+ rng = random.Random(seed)
78
+ a, b = rng.randint(1, 20), rng.randint(1, 20)
79
+ if 20 in (a, b) and min(a, b) < 20:
80
+ natural = _roll_natural_d20(_ctx(seed), "disadvantage")
81
+ assert natural == min(a, b)
82
+ assert _resolve_hit_outcome(natural, natural, 99, attack)[0] is False
83
+ return
84
+ pytest.fail("no seed with a discarded 20 in 500 tries")
@@ -0,0 +1,61 @@
1
+ """Regression — combat resolvers use Foundry-shaped bucket keys (Phase 6).
2
+
3
+ Codex review of Phase 6 caught that ``rules/combat.py`` was passing legacy
4
+ bucket strings (``"saving_throw"``, ``"damage"``, ``"attack_roll"``, ``"ac"``)
5
+ into ``apply_changes_to_check``. The seed templates emit Foundry-shaped keys
6
+ (``save.bonus``, ``damage.bonus``, ``attack.roll.bonus``, ``ac.bonus``); the
7
+ helper does exact key matching, so the resolvers were silently no-op-ing
8
+ every effect. These tests lock the alignment.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dnd5e_engine.types.effects import (
14
+ ActiveEffect,
15
+ ActiveEffectChange,
16
+ ActiveEffectDuration,
17
+ )
18
+
19
+
20
+ def _bless(target_id: str = "char:hero") -> ActiveEffect:
21
+ """Seed-shaped Bless: +1d4 on attack rolls and saves (Foundry keys)."""
22
+ return ActiveEffect(
23
+ id="effect:bless",
24
+ name="Bless",
25
+ origin="cast:bless:1",
26
+ target_id=target_id,
27
+ duration=ActiveEffectDuration(rounds=10),
28
+ changes=[
29
+ ActiveEffectChange(key="attack.roll.bonus", mode="add", value="1d4"),
30
+ ActiveEffectChange(key="save.bonus", mode="add", value="1d4"),
31
+ ],
32
+ flags={"concentration": True},
33
+ )
34
+
35
+
36
+ def _cloak(target_id: str = "char:hero") -> ActiveEffect:
37
+ """Seed-shaped Cloak of Protection: +1 AC and +1 saves (Foundry keys)."""
38
+ return ActiveEffect(
39
+ id="effect:cloak_of_protection",
40
+ name="Cloak of Protection",
41
+ origin="item:cloak_of_protection:1",
42
+ target_id=target_id,
43
+ changes=[
44
+ ActiveEffectChange(key="ac.bonus", mode="add", value=1),
45
+ ActiveEffectChange(key="save.bonus", mode="add", value=1),
46
+ ],
47
+ )
48
+
49
+
50
+ def _plus_one_sword(target_id: str = "char:hero") -> ActiveEffect:
51
+ """Seed-shaped +1 Weapon: +1 attack and +1 damage (Foundry keys)."""
52
+ return ActiveEffect(
53
+ id="effect:weapon_plus_1",
54
+ name="+1 Weapon",
55
+ origin="item:weapon_plus_1:1",
56
+ target_id=target_id,
57
+ changes=[
58
+ ActiveEffectChange(key="attack.roll.bonus", mode="add", value=1),
59
+ ActiveEffectChange(key="damage.bonus", mode="add", value=1),
60
+ ],
61
+ )
@@ -0,0 +1,25 @@
1
+ """Regression — combat resolvers honor flag-based advantage / disadvantage.
2
+
3
+ Codex Phase 6 review iter-12 P2: combat.py's saving_throw and
4
+ resolve_player_attack didn't read flag-based advantage / disadvantage
5
+ from active_effects, so Restrained / Guidance-style / Faerie Fire /
6
+ Invisible effects didn't shift the d20 mechanic in combat. Now both
7
+ mirror the iter-10/11 resolve_check fix.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dnd5e_engine.types.effects import (
13
+ ActiveEffect,
14
+ ActiveEffectChange,
15
+ )
16
+
17
+
18
+ def _flag_effect(key: str, target_id: str = "char:hero") -> ActiveEffect:
19
+ return ActiveEffect(
20
+ id=f"effect:test_{key}",
21
+ name=f"test_{key}",
22
+ origin=f"cast:test:{key}",
23
+ target_id=target_id,
24
+ changes=[ActiveEffectChange(key=key, mode="override", value=True)],
25
+ )
@@ -7,6 +7,7 @@ the supported ones.
7
7
  from __future__ import annotations
8
8
 
9
9
  import ast
10
+ import importlib.util
10
11
  import subprocess
11
12
  import sys
12
13
 
@@ -50,3 +51,8 @@ def test_gen2_entry_points_load_no_gen1_module() -> None:
50
51
  loaded = _loaded_modules()
51
52
  leaked = [m for m in loaded if m in GEN1]
52
53
  assert leaked == [], leaked
54
+
55
+
56
+ def test_gen1_modules_no_longer_exist() -> None:
57
+ present = [m for m in GEN1 if importlib.util.find_spec(m) is not None]
58
+ assert present == [], present
@@ -5,7 +5,6 @@ import dnd5e_engine
5
5
 
6
6
  TOP_LEVEL = {
7
7
  "AbilityScores",
8
- "ActionType",
9
8
  "ActiveEffect",
10
9
  "ActiveEffectChange",
11
10
  "ActiveEffectDuration",
@@ -54,21 +53,12 @@ TOP_LEVEL = {
54
53
  }
55
54
 
56
55
  PUBLIC_MODULES = [
57
- "dnd5e_engine.rules.combat",
58
56
  "dnd5e_engine.rules.conditions",
59
57
  "dnd5e_engine.rules.dice",
60
- "dnd5e_engine.rules.equipment",
61
- "dnd5e_engine.rules.gambits",
62
- "dnd5e_engine.rules.resolution",
63
58
  "dnd5e_engine.rules.skills",
64
- "dnd5e_engine.rules.spells",
65
- "dnd5e_engine.rules.combat_data",
66
- "dnd5e_engine.rules.combat_helpers",
67
59
  "dnd5e_engine.events",
68
- "dnd5e_engine.event_dicts",
69
60
  "dnd5e_engine.death_saves",
70
61
  "dnd5e_engine.outcome",
71
- "dnd5e_engine.dispatch",
72
62
  "dnd5e_engine.specs",
73
63
  "dnd5e_engine.spatial",
74
64
  "dnd5e_engine.lib_loader",
@@ -77,10 +67,8 @@ PUBLIC_MODULES = [
77
67
  "dnd5e_engine.build_spec",
78
68
  "dnd5e_engine.build_party",
79
69
  "dnd5e_engine.results",
80
- "dnd5e_engine.types.intent",
81
70
  "dnd5e_engine.types.combat",
82
71
  "dnd5e_engine.types.conditions",
83
- "dnd5e_engine.types.dice",
84
72
  "dnd5e_engine.types.effects",
85
73
  "dnd5e_engine.testing",
86
74
  ]