dnd5e-engine 0.4.0__tar.gz → 0.6.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 (279) hide show
  1. dnd5e_engine-0.6.0/CHANGELOG.md +1032 -0
  2. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/PKG-INFO +2 -2
  3. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/pyproject.toml +9 -7
  4. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/__init__.py +20 -24
  5. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/actor_stats.py +83 -0
  6. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/apply.py +215 -0
  7. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/attack.py +1308 -0
  8. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/build_context.py +579 -0
  9. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/check.py +66 -22
  10. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/conjuration.py +481 -0
  11. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/context.py +566 -0
  12. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/d20.py +97 -0
  13. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/damage.py +22 -2
  14. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/dice.py +13 -7
  15. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/effects.py +26 -5
  16. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/forced_movement.py +40 -0
  17. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/formula.py +44 -8
  18. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/heal.py +14 -0
  19. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/mastery.py +271 -0
  20. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/monster_actions.py +166 -2
  21. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/resolver.py +164 -0
  22. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/save.py +61 -17
  23. dnd5e_engine-0.6.0/src/dnd5e_engine/activities/save_primitive.py +340 -0
  24. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/scale.py +56 -24
  25. dnd5e_engine-0.6.0/src/dnd5e_engine/build_party.py +78 -0
  26. dnd5e_engine-0.6.0/src/dnd5e_engine/build_spec.py +776 -0
  27. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/check.py +13 -1
  28. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/death_saves.py +38 -10
  29. dnd5e_engine-0.6.0/src/dnd5e_engine/events.py +861 -0
  30. dnd5e_engine-0.6.0/src/dnd5e_engine/orchestrator.py +13464 -0
  31. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rest.py +69 -5
  32. dnd5e_engine-0.6.0/src/dnd5e_engine/rules/character.py +577 -0
  33. dnd5e_engine-0.6.0/src/dnd5e_engine/rules/choices.py +147 -0
  34. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/conditions.py +179 -26
  35. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/skills.py +92 -12
  36. dnd5e_engine-0.6.0/src/dnd5e_engine/rules/uses.py +93 -0
  37. dnd5e_engine-0.6.0/src/dnd5e_engine/spatial.py +636 -0
  38. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/specs.py +121 -10
  39. dnd5e_engine-0.6.0/src/dnd5e_engine/spellcasting.py +312 -0
  40. dnd5e_engine-0.6.0/src/dnd5e_engine/turn_lifecycle.py +128 -0
  41. dnd5e_engine-0.6.0/src/dnd5e_engine/types/__init__.py +18 -0
  42. dnd5e_engine-0.6.0/src/dnd5e_engine/types/combat.py +388 -0
  43. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/types/effects.py +45 -5
  44. dnd5e_engine-0.6.0/src/dnd5e_engine/views.py +220 -0
  45. dnd5e_engine-0.6.0/tests/activities/test_actor_stats.py +51 -0
  46. dnd5e_engine-0.6.0/tests/activities/test_apply_damage_magical.py +105 -0
  47. dnd5e_engine-0.6.0/tests/activities/test_attack_advantage_live.py +276 -0
  48. dnd5e_engine-0.6.0/tests/activities/test_attack_condition_rows.py +169 -0
  49. dnd5e_engine-0.6.0/tests/activities/test_attack_unseen.py +90 -0
  50. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_build_context.py +70 -6
  51. dnd5e_engine-0.6.0/tests/activities/test_c18_trait_consumers.py +345 -0
  52. dnd5e_engine-0.6.0/tests/activities/test_conjuration_routing.py +259 -0
  53. dnd5e_engine-0.6.0/tests/activities/test_d20.py +57 -0
  54. dnd5e_engine-0.6.0/tests/activities/test_damage_count_marker_scaling.py +79 -0
  55. dnd5e_engine-0.6.0/tests/activities/test_monster_action_ranking.py +216 -0
  56. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_monster_actions.py +61 -16
  57. dnd5e_engine-0.6.0/tests/activities/test_multiattack_opaque_ids.py +48 -0
  58. dnd5e_engine-0.6.0/tests/activities/test_resolve_check_activity.py +783 -0
  59. dnd5e_engine-0.6.0/tests/activities/test_save_dc_and_damage_exprs.py +160 -0
  60. dnd5e_engine-0.6.0/tests/activities/test_save_ignore_cover.py +42 -0
  61. dnd5e_engine-0.6.0/tests/activities/test_save_magic_resistance.py +64 -0
  62. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_save_primitive.py +202 -26
  63. dnd5e_engine-0.6.0/tests/activities/test_summon_registry.py +241 -0
  64. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_weapon_mastery.py +63 -15
  65. dnd5e_engine-0.6.0/tests/c20_support.py +112 -0
  66. dnd5e_engine-0.6.0/tests/c21_support.py +206 -0
  67. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/harness.py +12 -1
  68. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c06_reactions.py +10 -3
  69. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c07_sneak_and_repertoire.py +59 -23
  70. dnd5e_engine-0.6.0/tests/e2e/test_c12_conditions.py +481 -0
  71. dnd5e_engine-0.6.0/tests/e2e/test_c13_concentration.py +445 -0
  72. dnd5e_engine-0.6.0/tests/e2e/test_c14_action_economy.py +628 -0
  73. dnd5e_engine-0.6.0/tests/e2e/test_c15_attack_rules.py +542 -0
  74. dnd5e_engine-0.6.0/tests/e2e/test_c16_spatial_targeting.py +758 -0
  75. dnd5e_engine-0.6.0/tests/e2e/test_c17_slots_and_rests.py +299 -0
  76. dnd5e_engine-0.6.0/tests/e2e/test_c18_monster_economy.py +629 -0
  77. dnd5e_engine-0.6.0/tests/e2e/test_c19_character_derivation.py +387 -0
  78. dnd5e_engine-0.6.0/tests/e2e/test_c20_class_features.py +805 -0
  79. dnd5e_engine-0.6.0/tests/e2e/test_c21_summons.py +494 -0
  80. dnd5e_engine-0.6.0/tests/e2e/test_c22_dataset.py +506 -0
  81. dnd5e_engine-0.6.0/tests/test_action_economy_attacks.py +761 -0
  82. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_active_effect_resistance_fold.py +8 -2
  83. dnd5e_engine-0.6.0/tests/test_active_effect_statuses_order.py +64 -0
  84. dnd5e_engine-0.6.0/tests/test_attack_classification.py +80 -0
  85. dnd5e_engine-0.6.0/tests/test_attack_range_tiers.py +433 -0
  86. dnd5e_engine-0.6.0/tests/test_build_party_c19.py +169 -0
  87. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_build_party_passives.py +2 -1
  88. dnd5e_engine-0.6.0/tests/test_build_spec_multiclass.py +100 -0
  89. dnd5e_engine-0.6.0/tests/test_c16_orchestrator.py +955 -0
  90. dnd5e_engine-0.6.0/tests/test_c16_spatial.py +243 -0
  91. dnd5e_engine-0.6.0/tests/test_c16b_vision.py +714 -0
  92. dnd5e_engine-0.6.0/tests/test_c17_slots.py +550 -0
  93. dnd5e_engine-0.6.0/tests/test_c18_monster_economy_units.py +1428 -0
  94. dnd5e_engine-0.6.0/tests/test_c20_action_surge.py +232 -0
  95. dnd5e_engine-0.6.0/tests/test_c20_bardic_inspiration.py +245 -0
  96. dnd5e_engine-0.6.0/tests/test_c20_cunning_action.py +106 -0
  97. dnd5e_engine-0.6.0/tests/test_c20_feature_uses.py +303 -0
  98. dnd5e_engine-0.6.0/tests/test_c20_fighting_styles.py +223 -0
  99. dnd5e_engine-0.6.0/tests/test_c20_multiclass_live.py +122 -0
  100. dnd5e_engine-0.6.0/tests/test_c20_rage.py +216 -0
  101. dnd5e_engine-0.6.0/tests/test_c20_unarmed_and_monk.py +321 -0
  102. dnd5e_engine-0.6.0/tests/test_c21_concentration_anchor.py +262 -0
  103. dnd5e_engine-0.6.0/tests/test_c21_magic_weapon.py +298 -0
  104. dnd5e_engine-0.6.0/tests/test_c21_polymorph.py +382 -0
  105. dnd5e_engine-0.6.0/tests/test_c21_roster_events.py +95 -0
  106. dnd5e_engine-0.6.0/tests/test_c21_roster_primitives.py +358 -0
  107. dnd5e_engine-0.6.0/tests/test_c21_spiritual_weapon.py +495 -0
  108. dnd5e_engine-0.6.0/tests/test_c21_stat_block_commands.py +321 -0
  109. dnd5e_engine-0.6.0/tests/test_c21_summon_allegiance.py +566 -0
  110. dnd5e_engine-0.6.0/tests/test_c21_summon_dragon.py +485 -0
  111. dnd5e_engine-0.6.0/tests/test_c21_summon_turns.py +523 -0
  112. dnd5e_engine-0.6.0/tests/test_c21_surface.py +87 -0
  113. dnd5e_engine-0.6.0/tests/test_c21_transform_core.py +390 -0
  114. dnd5e_engine-0.6.0/tests/test_c21_wild_shape.py +393 -0
  115. dnd5e_engine-0.6.0/tests/test_capability_matrix.py +827 -0
  116. dnd5e_engine-0.6.0/tests/test_combat_bucket_keys.py +61 -0
  117. dnd5e_engine-0.6.0/tests/test_combat_flag_advantage.py +25 -0
  118. dnd5e_engine-0.6.0/tests/test_concentration_check_event.py +193 -0
  119. dnd5e_engine-0.6.0/tests/test_concentration_lifecycle_drops.py +261 -0
  120. dnd5e_engine-0.6.0/tests/test_concentration_save_modifier.py +102 -0
  121. dnd5e_engine-0.6.0/tests/test_concentration_timed_expiry.py +168 -0
  122. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_cover_bonus_sidecar.py +20 -10
  123. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_coverage_backfill_death_saves.py +24 -0
  124. dnd5e_engine-0.6.0/tests/test_death_saves_exhaustion.py +54 -0
  125. dnd5e_engine-0.6.0/tests/test_derive_sheet.py +605 -0
  126. dnd5e_engine-0.6.0/tests/test_dodge_help_hide.py +834 -0
  127. dnd5e_engine-0.6.0/tests/test_effect_timed_expiry.py +474 -0
  128. dnd5e_engine-0.6.0/tests/test_end_combat_retention.py +137 -0
  129. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_gen2_has_no_gen1_imports.py +6 -0
  130. dnd5e_engine-0.6.0/tests/test_grapple_shove_stand_up.py +564 -0
  131. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_grid_topology.py +14 -9
  132. dnd5e_engine-0.6.0/tests/test_incapacitated_by_effect.py +179 -0
  133. dnd5e_engine-0.6.0/tests/test_initiative_rolling.py +305 -0
  134. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_live_combat_view.py +44 -1
  135. dnd5e_engine-0.6.0/tests/test_loading_property.py +217 -0
  136. dnd5e_engine-0.6.0/tests/test_monster_legacy_swing.py +121 -0
  137. dnd5e_engine-0.6.0/tests/test_opportunity_attack_primitive.py +279 -0
  138. dnd5e_engine-0.6.0/tests/test_orchestrator_condition_gates.py +824 -0
  139. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_grid_combat.py +48 -3
  140. dnd5e_engine-0.6.0/tests/test_orchestrator_hydration_proficiencies.py +118 -0
  141. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_pc_resolution_typed.py +4 -1
  142. dnd5e_engine-0.6.0/tests/test_orchestrator_trait_hydration.py +58 -0
  143. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_party_member_reach_ft.py +2 -0
  144. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_public_api_surface.py +11 -12
  145. dnd5e_engine-0.6.0/tests/test_repeat_save_runs_once_per_turn.py +168 -0
  146. dnd5e_engine-0.6.0/tests/test_resolve_check_c19.py +77 -0
  147. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rest.py +95 -0
  148. dnd5e_engine-0.6.0/tests/test_rules_character.py +258 -0
  149. dnd5e_engine-0.6.0/tests/test_rules_choices.py +67 -0
  150. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_combat_data_and_skills.py +0 -149
  151. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_conditions.py +174 -2
  152. dnd5e_engine-0.6.0/tests/test_rules_dice_equipment_spells.py +302 -0
  153. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_effects_and_gambits.py +0 -133
  154. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_smoke.py +1 -8
  155. dnd5e_engine-0.6.0/tests/test_rules_uses.py +56 -0
  156. dnd5e_engine-0.6.0/tests/test_save_cover_hydration.py +155 -0
  157. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_scale_resolver.py +29 -0
  158. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_sneak_attack_resolver.py +18 -8
  159. dnd5e_engine-0.6.0/tests/test_spatial_distance.py +32 -0
  160. dnd5e_engine-0.6.0/tests/test_spellcasting.py +197 -0
  161. dnd5e_engine-0.6.0/tests/test_turn_lifecycle.py +318 -0
  162. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_types_smoke.py +0 -73
  163. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_use_feature_intent.py +27 -12
  164. dnd5e_engine-0.6.0/tests/test_versatile_and_attribution.py +331 -0
  165. dnd5e_engine-0.6.0/tests/test_weapon_masteries_c15.py +1140 -0
  166. dnd5e_engine-0.6.0/tests/test_weapon_proficiency.py +333 -0
  167. dnd5e_engine-0.4.0/CHANGELOG.md +0 -241
  168. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/apply.py +0 -102
  169. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/attack.py +0 -639
  170. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/build_context.py +0 -323
  171. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/context.py +0 -254
  172. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/mastery.py +0 -209
  173. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/resolver.py +0 -90
  174. dnd5e_engine-0.4.0/src/dnd5e_engine/activities/save_primitive.py +0 -205
  175. dnd5e_engine-0.4.0/src/dnd5e_engine/build_party.py +0 -138
  176. dnd5e_engine-0.4.0/src/dnd5e_engine/build_spec.py +0 -101
  177. dnd5e_engine-0.4.0/src/dnd5e_engine/dispatch.py +0 -367
  178. dnd5e_engine-0.4.0/src/dnd5e_engine/event_dicts.py +0 -50
  179. dnd5e_engine-0.4.0/src/dnd5e_engine/events.py +0 -503
  180. dnd5e_engine-0.4.0/src/dnd5e_engine/orchestrator.py +0 -5523
  181. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/_class_meta.py +0 -14
  182. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/_parsing.py +0 -36
  183. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat.py +0 -595
  184. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat_data.py +0 -243
  185. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat_helpers.py +0 -338
  186. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/equipment.py +0 -121
  187. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/gambits.py +0 -193
  188. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/resolution.py +0 -180
  189. dnd5e_engine-0.4.0/src/dnd5e_engine/rules/spells.py +0 -165
  190. dnd5e_engine-0.4.0/src/dnd5e_engine/spatial.py +0 -380
  191. dnd5e_engine-0.4.0/src/dnd5e_engine/types/__init__.py +0 -49
  192. dnd5e_engine-0.4.0/src/dnd5e_engine/types/combat.py +0 -229
  193. dnd5e_engine-0.4.0/src/dnd5e_engine/types/dice.py +0 -46
  194. dnd5e_engine-0.4.0/src/dnd5e_engine/types/intent.py +0 -187
  195. dnd5e_engine-0.4.0/src/dnd5e_engine/views.py +0 -68
  196. dnd5e_engine-0.4.0/tests/activities/test_resolve_check_activity.py +0 -349
  197. dnd5e_engine-0.4.0/tests/test_capability_matrix.py +0 -125
  198. dnd5e_engine-0.4.0/tests/test_combat_bucket_keys.py +0 -177
  199. dnd5e_engine-0.4.0/tests/test_combat_flag_advantage.py +0 -154
  200. dnd5e_engine-0.4.0/tests/test_gen1_deprecation_warnings.py +0 -63
  201. dnd5e_engine-0.4.0/tests/test_generation_parity_attack.py +0 -179
  202. dnd5e_engine-0.4.0/tests/test_rules_combat_helpers.py +0 -654
  203. dnd5e_engine-0.4.0/tests/test_rules_combat_resolution.py +0 -802
  204. dnd5e_engine-0.4.0/tests/test_rules_dice_equipment_spells.py +0 -563
  205. dnd5e_engine-0.4.0/tests/test_rules_resolution_and_parsing.py +0 -368
  206. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/.gitignore +0 -0
  207. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/LICENSE +0 -0
  208. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/Makefile +0 -0
  209. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/NOTICE +0 -0
  210. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/README.md +0 -0
  211. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/scripts/_smoke_grid_combat.py +0 -0
  212. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/scripts/smoke_clean_install.sh +0 -0
  213. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/__init__.py +0 -0
  214. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/cast.py +0 -0
  215. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/passive_stats.py +0 -0
  216. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/lib_loader.py +0 -0
  217. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/outcome.py +0 -0
  218. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/py.typed +0 -0
  219. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/results.py +0 -0
  220. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/__init__.py +0 -0
  221. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/dice.py +0 -0
  222. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/effects.py +0 -0
  223. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/testing.py +0 -0
  224. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/types/conditions.py +0 -0
  225. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/__init__.py +0 -0
  226. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/__init__.py +0 -0
  227. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_cast_level_override.py +0 -0
  228. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_multiattack_fidelity.py +0 -0
  229. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/__init__.py +0 -0
  230. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/conftest.py +0 -0
  231. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
  232. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
  233. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
  234. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c03_effect_modes.py +0 -0
  235. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
  236. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c05_spatial.py +0 -0
  237. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c08_passive_projection.py +0 -0
  238. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c09_rest.py +0 -0
  239. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
  240. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c11_item_charges.py +0 -0
  241. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_active_effect_schema.py +0 -0
  242. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_apply_changes_to_check.py +0 -0
  243. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_apply_modifiers.py +0 -0
  244. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_build_context_abilities.py +0 -0
  245. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_build_party.py +0 -0
  246. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_concentration_drop_effect_name.py +0 -0
  247. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_concentration_repeat_save_identity.py +0 -0
  248. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
  249. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_determinism_contract.py +0 -0
  250. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_docstrings_are_host_agnostic.py +0 -0
  251. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_duration_tick_at_turn_end.py +0 -0
  252. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_effects_builder_internals.py +0 -0
  253. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_enchantment_sidecar_projection.py +0 -0
  254. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_end_combat_snapshot.py +0 -0
  255. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_expire_clears_active_conditions.py +0 -0
  256. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
  257. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_get_actor_active_effects.py +0 -0
  258. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_iter11_fixes.py +0 -0
  259. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_iter13_fixes.py +0 -0
  260. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_iter5_fixes.py +0 -0
  261. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_lib_loader.py +0 -0
  262. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_lib_loader_configure.py +0 -0
  263. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_monster_dash.py +0 -0
  264. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_monster_flee_retreat.py +0 -0
  265. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_gating_typed.py +0 -0
  266. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_item_charges.py +0 -0
  267. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_monster_typed.py +0 -0
  268. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
  269. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_passive_stats.py +0 -0
  270. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_phase6_public_surface.py +0 -0
  271. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rage_second_wind_e2e.py +0 -0
  272. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_reaction_queue.py +0 -0
  273. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_resolve_check.py +0 -0
  274. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_resolve_check_flag_advantage.py +0 -0
  275. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_resolve_check_specific_buckets.py +0 -0
  276. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_smoke.py +0 -0
  277. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_start_combat_active_effects.py +0 -0
  278. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
  279. {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
@@ -0,0 +1,1032 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.6.0]
11
+
12
+ Lockstep release with `dnd5e-srd-data` 0.6.0 and `nat20-bridge` 0.6.0. The
13
+ engine now depends on `dnd5e-srd-data>=0.6.0` (it reads the typed monster
14
+ traits and `spellcasting_ability`, the legendary pool sizes, and now-filled
15
+ `Monster.ac` values for every monster); `nat20-bridge` requires both at
16
+ `>=0.6.0`.
17
+
18
+ Core-mechanics **foundations** (F1 actor stat projection, F2 unified d20 test,
19
+ F3 turn lifecycle) and clusters **C12 — conditions enforced**, **C13 —
20
+ concentration lifecycle**, **C14 — action economy** (Dodge, Help, Hide,
21
+ Extra Attack, two-weapon fighting, Grapple/Shove/`stand_up`, engine-rolled
22
+ initiative with Surprise, and opportunity attacks through the shared d20
23
+ primitive), **C15 — attack rules** (weapon proficiency, range tiers,
24
+ thrown weapons, Ranged Attacks in Close Combat, Heavy, versatile grip,
25
+ damage attribution, crit-at-0-HP, Loading, and all eight 2024 weapon
26
+ masteries), **C16 — grid AoE targeting** (typed-template enumeration with
27
+ line-of-effect, creature cover, multi-cell grid movement and forced
28
+ movement), **C16b — vision & light** (scene lighting/obscurement and the
29
+ `_combatant_can_see` composite gating Dodge, Ranged Attacks in Close Combat,
30
+ Opportunity Attacks, Hide and Frightened), **C17 — spell slots, rests and
31
+ upcasting** (per-class/
32
+ multiclass/Pact Magic slot derivation, rest-based slot recovery and
33
+ Exhaustion reduction, upcast target-count scaling, Counterspell/readied-cast
34
+ slot+range gating, and out-of-combat Ritual resolution), **C18 — monster
35
+ action economy** (Recharge, Regeneration, Legendary Actions, Legendary
36
+ Resistance, stat-block spellcasting, and the remaining `MonsterTraitMechanic`
37
+ consumers), **C19 — character derivation** (`derive_sheet`/`DerivedSheet`,
38
+ `build_party_member` explicit-wins merge, `CheckSpec` Jack of All Trades /
39
+ Reliable Talent), **C20 — class feature mechanics** (Fighting Style feats,
40
+ Martial Arts, Flurry of Blows and the Focus economy, Action Surge, Bardic
41
+ Inspiration on attack rolls, Rage's end conditions, Lay on Hands' pool,
42
+ feature-gated Cunning Action, every corpus limited-use cap, and live
43
+ multiclass), **C21a — summons foundations** (every concentration spell
44
+ concentrates; Magic Weapon; Spiritual Weapon's caster-owned force; Wild Shape
45
+ and Polymorph as stat-block swaps with Temporary Hit Points; stat-block attack
46
+ commands) and **C21b — roster summons** (Summon Dragon's Draconic Spirit joins
47
+ the initiative order right after its caster, takes the Dodge action unless
48
+ commanded, and leaves at 0 Hit Points or with its caster's concentration),
49
+ closed out by **C23** (a bounded live-combat registry, sorted
50
+ `ActiveEffect.statuses`, three rules corrections and the 0.6.0 floors).
51
+ Nothing public is removed or renamed, but C19 reshapes two
52
+ existing `CombatInstance` fields — `ac` and `attack_bonus` widen from
53
+ defaulted `int`s to `int | None = None`, matching `hp_max`/`hp_current`/
54
+ `base_speed` — and every other new field across this release is optional
55
+ and defaults to the pre-0.6 behaviour.
56
+ C12/C14/C15/C17/C18 do change *results* for hosts that carry conditions,
57
+ exhaustion, turn-keeping attacks, weapon proficiency/mastery data, casters,
58
+ or monsters with a recharge/limited-use/legendary action or a
59
+ newly-consumed trait on a combatant; C19 changes results for hosts that
60
+ build through `build_party_member` and leave `ac`/`attack_bonus`/`hp_max`/
61
+ `hp_current`/`base_speed` unset (now derived instead of defaulted), or that
62
+ rely on the save/skill/weapon proficiencies it now derives.
63
+ C20 changes results for raging barbarians, Rogue 1s taking a Bonus-Action
64
+ Dash, any creature without Cunning Action sent a `disengage` with
65
+ `use_bonus_action=True` (now refused with
66
+ `IntentRejectedError("no_action_economy")` where it used to spend the Action),
67
+ newly capped features and unarmed strikes, for hosts that send `classes`,
68
+ `feats` or `fighting_style`, and wherever an effect makes a concentrating
69
+ caster or a grappler Incapacitated.
70
+ C21a changes results wherever a concentration spell applies no concentration
71
+ effect of its own (it now concentrates, so an earlier concentration ends and
72
+ later damage draws a Constitution save), for readied concentration spells,
73
+ for Magic Weapon, Wild Shape and Polymorph intents that name no weapon or
74
+ form (now refused before anything is spent), and for a host-driven attacker
75
+ with Pack Tactics (it now gets its Advantage).
76
+ C21b changes results only where a summon is in play: a Summon Dragon cast
77
+ seats a creature, or is refused before anything is spent when it has no legal
78
+ space; a `ready` that names it is refused; and foes may attack the creature.
79
+ Behavioural deltas (and the fixtures they move) are enumerated in
80
+ [`docs/migration/v0.5-to-v0.6.md`](../../docs/migration/v0.5-to-v0.6.md).
81
+
82
+ - **Action economy (C14).** Extra Attack reads a caster's granted
83
+ `extra-attack` / `two-extra-attacks` / `three-extra-attacks` feature slugs
84
+ (highest tier wins, never summed) into a per-Action `attacks_remaining`
85
+ counter; a turn now keeps going after a main-hand attack until that
86
+ counter — and any open two-weapon-fighting Bonus Action window — is spent
87
+ (`LiveCombatView.turn: TurnCombatView`). A Light main-hand weapon opens a
88
+ same-turn off-hand Bonus Action swing (SRD 5.2 §Two-Weapon Fighting). Dodge,
89
+ Help (assist-an-attack-roll flavor) and Hide (DC 15 Stealth, cover/
90
+ obscurement gate, Invisible-while-hidden) now have live dispatch handlers.
91
+ New `IntentType` members `"grapple"`, `"shove"`, `"stand_up"`,
92
+ `"escape_grapple"` resolve the Unarmed Strike Grapple/Shove options, the
93
+ SRD 5.2 "Ending a Grapple" escape check and standing from Prone at half
94
+ Speed. `PlayerIntent.shove_push: bool = False` picks the shover's Prone-or-
95
+ push outcome. `PartyMemberSpec.initiative` / `EncounterMemberSpec.initiative`
96
+ widen to `int | None`: `None` rolls an engine `d20 + DEX modifier` (spec
97
+ order, before all other combat draws), with Disadvantage from the new
98
+ `is_surprised: bool = False` field or a seeded incapacitated-implying
99
+ status. Opportunity attacks now roll through `roll_d20_test` like every
100
+ other attack, picking up condition/Dodge/Exhaustion sources.
101
+
102
+ - **Concentration lifecycle (C13).** The damage-triggered save DC is capped at
103
+ the SRD 5.2 maximum of 30. A new concentration cast cascades the drop of the
104
+ caster's prior concentration effect (SRD "the moment you start casting…").
105
+ Concentration now ends on death and on gaining any Incapacitated-implying
106
+ condition. New `IntentType` member `"drop_concentration"`: a voluntary,
107
+ turn-keeping drop that costs no action economy. Concentration spells now
108
+ expire at their maximum typed duration (1 minute = 10 rounds, …) at the
109
+ caster's turn end via the new `engine:concentration-expiry` turn hook, with
110
+ `EffectExpired(reason="duration")`. `LiveCombatView.concentration_chain`
111
+ projects the caster-keyed ownership map for hosts.
112
+
113
+ - **Attack rules (C15).** Weapon proficiency is a real gate: a host that never
114
+ sets `PartyMemberSpec.weapon_proficiencies` is assumed proficient (the R1
115
+ sentinel — legacy behaviour, byte-identical); an explicit (possibly empty)
116
+ list enforces proficiency by the weapon's category or slug, omitting
117
+ Proficiency Bonus (never subtracting it) when unproficient. Attack range now
118
+ has three tiers — normal, a middle disadvantage tier beyond normal range
119
+ (`"range:long"`), and beyond max range is rejected — and a melee weapon with
120
+ the Thrown property can now attack at range using its throw bands (both were
121
+ previously flatly rejected). Ranged Attacks in Close Combat (SRD 5.2):
122
+ disadvantage when a living, sighted, non-Incapacitated hostile is within 5
123
+ ft of the attacker (`"ranged_in_melee"`). Heavy weapons impose disadvantage
124
+ on a wielder with a raw Strength score below 13 — this and the Vex/Sap
125
+ mastery riders below all ride the same `"trait"` advantage-source token.
126
+ `PlayerIntent.two_handed: bool = False` selects a Versatile weapon's
127
+ two-handed damage die on a melee swing. `DamageApplied` gains `source_id`
128
+ (weapon slug / synthesized activity id / `"mastery:<slug>"`; spell/save/heal
129
+ damage paths still report `None`, a C17+ seam) and `is_crit`; a critical hit
130
+ against a target already making death saves now counts as two failures (SRD
131
+ §Damage at 0 Hit Points). Loading weapons cap at one fired shot per actor
132
+ per turn (`AttackFailed(reason="weapon_already_fired")`, gated ahead of the
133
+ Charmed-target gate). All eight 2024 weapon masteries are live: Graze
134
+ (miss-damage), Topple (Con save vs. `prone`, honoring condition immunity),
135
+ Vex (a 2-round Advantage grant against the same target), Sap (a
136
+ Disadvantage mark on the target until the source's next turn), Slow (a
137
+ flat, non-stacking −10 ft Speed penalty, cleared at the source's next turn
138
+ start), Push (a full 10 ft forced move via `push_combatant`; the "Large or
139
+ smaller" size gate is unmodelled), Cleave (a deterministic, once-per-turn
140
+ chained attack against the nearest eligible living hostile within 5 ft of
141
+ the first target and within reach) and Nick (the off-hand extra attack
142
+ spends no Bonus Action — SRD 5.2 lets it ride the Attack action instead).
143
+
144
+ - **Spell slots, rests and upcasting (C17).** `spellcasting.py` derives
145
+ per-class Spellcasting slots (`derive_spell_slots`), multiclass slots
146
+ (`derive_multiclass_slots`, per-class half/third rounding summed, SRD
147
+ §Multiclassing) and Pact Magic (`derive_pact_slots`,
148
+ `derive_multiclass_pact_slots`); `CharacterBuildSpec.classes: dict[str,
149
+ int]` is the multiclass carrier, reconciled with the existing
150
+ `class_slug`/`level` single-class fields. `resolve_long_rest` restores
151
+ Spellcasting/Pact Magic slots and reduces Exhaustion by 1 (floored at 0);
152
+ `resolve_short_rest` restores only Pact Magic (SRD's only
153
+ Short-Rest-recovering slot pool). A Magic Missile-shaped cast now scales
154
+ its DART COUNT, not just its damage dice, via `target.affects.count` /
155
+ `spellcasting.resolve_target_count` and `PlayerIntent.target_ids`. An
156
+ armed Counterspell/readied-cast reaction is now gated on slot
157
+ availability at its readied level and, for Counterspell, its own 60 ft
158
+ range with line of sight — an ineligible reactor's reaction is skipped,
159
+ not consumed. A new `SpellCast` event carries component/material
160
+ metadata (never enforced) on every PC cast path. `PlayerIntent.
161
+ as_ritual` + `spellcasting.resolve_ritual_cast` resolve Ritual-tagged
162
+ spells out-of-combat only; an in-combat ritual cast is rejected
163
+ (`CastFailedReason` gains `"ritual_in_combat"`). An **upcast** Magic
164
+ Missile previously over-rolled its single dart (`Nd4+1`, reading the
165
+ corpus's empty `scaling.mode` as whole-mode scaling); it now rolls
166
+ `1d4+1` once per cast and applies it to N darts, changing the RNG draw
167
+ count for that one path (base-level Magic Missile is unaffected). See
168
+ the migration guide for the full behavioural-delta list, including the
169
+ Magic Missile event-count change and the `build_party_member`
170
+ empty-pool fallback.
171
+
172
+ - **Monster action economy (C18).** A living monster's driven turn now
173
+ runs legendary-pool reset, Recharge rolls (`RechargeRolled`, gates a spent
174
+ recharge action until it succeeds) and Regeneration (unconditional — the
175
+ bundled corpus trait text carries no suppression clause) before action
176
+ selection; a dead monster runs none of them.
177
+ `activities/monster_actions.py::rank_monster_actions` ranks a live
178
+ recharge action first, a Spellcasting action whose N/Day offensive spell
179
+ still has a use left second (it casts that spell, not an at-will one),
180
+ `multiattack` third, then everything else (at-will spells included).
181
+ Stat-block monster spellcasting now selects and casts: the spell book
182
+ comes from the compendium uuids already in the data, the save DC and the
183
+ spell attack bonus use the monster's own `Combatant.spellcasting_ability`
184
+ (new dataset field `Monster.spellcasting_ability`) against its own
185
+ ability score + proficiency bonus, and `SpellCast` is now emitted on the
186
+ monster path too. Legendary Actions: `advance_monster_turn(handle, *,
187
+ legendary=True, actor_id=...)` spends one legendary action from a pool
188
+ that refills at the start of the creature's own turn
189
+ (`LegendaryActionUsed`; pool size from the new dataset field
190
+ `Monster.legendary_action_uses`, 3 for every bundled legendary monster).
191
+ Legendary Resistance: a per-day pool (never reset at turn start) sized
192
+ from the new dataset field `Monster.legendary_resistance_uses` (3, 4 or
193
+ 6); a new top-level `resolve_legendary_resistance(handle, entity_id) ->
194
+ int` pre-arms a conversion of the entity's next failed save — every save
195
+ path in the engine honors it, including the Grapple/Shove Unarmed Strike
196
+ save and the damage-triggered concentration check — and emits
197
+ `LegendaryResistanceUsed` after the converted `SaveRolled`. Five more `MonsterTraitMechanic`
198
+ values are now consumed: Pack Tactics (attack advantage from an adjacent,
199
+ non-Incapacitated ally), Sunlight Sensitivity (attack disadvantage via
200
+ the new `GridScene.sunlight: bool = False` scene flag), Undead Fortitude
201
+ (a CON save to hold at 1 HP instead of dropping to 0, live-path
202
+ included), Swarm (no HP or temp HP gain) and Legendary Resistance itself.
203
+ Corpus damage resistances/immunities now hydrate from the monster
204
+ template when a host leaves both fields empty, unconditionally (no
205
+ "nonmagical attacks" qualifier, matching SRD 5.2 stat blocks). `Combatant.
206
+ has_fled: bool = False` persists a fleeing monster's stance across turns,
207
+ and `ended_reason`/`CombatEnded.reason` can now actually be `"flee"` when
208
+ every living foe has fled. See the migration guide for the full
209
+ determinism-affecting delta list — a seeded replay reaching a monster
210
+ turn with a recharge action, an unspent N/Day spell, a legendary action or
211
+ a newly-consumed trait diverges from that point on.
212
+
213
+ - **Class feature mechanics (C20).** The four SRD 5.2 Fighting Style feats
214
+ apply: Defense (+1 AC from `derive_sheet` while armor is worn), Archery (+2
215
+ to Ranged-weapon attack rolls, on top of a pinned `attack_bonus`), Great
216
+ Weapon Fighting (a 1 or 2 on a damage die counts as a 3, with no extra draw,
217
+ for a weapon held in two hands and so never beside a Shield) and Two-Weapon
218
+ Fighting (the Light off-hand swing keeps its ability modifier). Martial
219
+ Arts: an unarmored, shieldless monk's Unarmed Strike or
220
+ Monk weapon rolls the Martial Arts die when it beats the weapon's own, uses
221
+ DEX when higher (the Grapple and Shove DC too), and gets a Bonus Unarmed
222
+ Strike; Flurry of Blows spends 1 Focus Point for the next two Unarmed
223
+ Strikes (three with Heightened Focus). Action Surge grants one extra action
224
+ — never a Magic action, one surge per turn — counted on
225
+ `LiveCombatView.turn.extra_actions_remaining`, and a `special`-activation
226
+ feature that resolves nothing by itself (Action Surge, Sacred Weapon) costs
227
+ no Action. A Bardic Inspiration die is redeemed on a failed attack roll
228
+ (`PlayerIntent.redeem_granted_die`). Rage ends at the end of the
229
+ barbarian's next turn unless an attack roll against an enemy, an enemy's
230
+ saving throw or a Bonus Action (`use_feature rage` again, no use spent)
231
+ extends it, and it ends at once on the Incapacitated condition, however it
232
+ is applied; a Barbarian 15's Persistent Rage needs no extension and ends
233
+ early only on Unconscious. Lay on Hands' Heal draws
234
+ `PlayerIntent.pool_points` from a pool of five times the Paladin level.
235
+ Cunning Action's Bonus-Action Dash and Disengage read the feature at the
236
+ Rogue's own level (Rogue 2), not `class_slug`. Every corpus
237
+ `uses.max` (`@prof`, `max(1, @abilities.<ab>.mod)`,
238
+ `N * @classes.<class>.levels`) now caps its feature, and an invocation
239
+ spends its activity's `consumption.targets` cost.
240
+ `PartyMemberSpec.classes` carries a multiclass character's per-class levels
241
+ into combat: features, scale values and `@classes.<class>.levels` per class.
242
+ See the migration guide for every delta and the determinism notes.
243
+
244
+ - **Summons foundations (C21a).** Every concentration spell concentrates: one
245
+ that applies no concentration effect of its own (Spiritual Weapon, a Hold
246
+ Person every target saved against, a Polymorph whose save succeeded) leaves
247
+ a caster-held anchor effect, on a turn, a monster's cast or a readied cast.
248
+ Four SRD 5.2 sources on an allowlist resolve. Magic Weapon enchants the
249
+ weapon `PlayerIntent.weapon_id` names (+1/+2/+3 to hit and damage, magical,
250
+ on top of a pinned `attack_bonus`; a recast ends the old one; never an
251
+ Unarmed Strike). Spiritual Weapon places a caster-owned force that attacks
252
+ at once and, as a Bonus Action on later turns, moves up to 20 feet and
253
+ attacks again (`attack` with `spell_id`); it ends with its concentration,
254
+ and no monster casts it (BACKLOG). Wild Shape and Polymorph swap in the Beast
255
+ stat block `PlayerIntent.form_id` names, checked before anything is spent
256
+ (the Beast Shapes table; a Challenge Rating up to the target's), with
257
+ ordinary Temporary Hit Points — the Druid level, or the form's Hit Points.
258
+ Wild Shape ends on reuse, Incapacitated, death or a Bonus-Action leave;
259
+ Polymorph with its concentration, when its Temporary Hit Points are gone
260
+ (the rest of that hit landing on the creature) or with the combat (its grant
261
+ stays out of `CombatOutcome.residual_temp_hp`). A transformed creature can't
262
+ cast, ready or release a readied spell, or make a weapon attack; it attacks
263
+ with its form's actions through `PlayerIntent.stat_block_action_id` (or on
264
+ its own monster turn) at the form's real ability scores and Proficiency
265
+ Bonus, with the form's Multiattack count. See the migration guide for every
266
+ delta.
267
+
268
+ - **Roster summons (C21b).** Summon Dragon seats a Draconic Spirit in an
269
+ unoccupied space its caster can see within 60 feet (`target_zone_id`, else
270
+ the nearest free cell), in the initiative slot right after its caster at the
271
+ caster's count, with the spell's numbers: AC 14 + the slot level, HP 50 + 10
272
+ per slot level above 5, the caster's Proficiency Bonus, and half the slot
273
+ level (round down) of Rends per Attack action at the caster's spell attack
274
+ bonus for 1d6 + 4 + the slot level. `advance_monster_turn` has it take the
275
+ Dodge action when uncommanded (a pass when it can't act); a host commands
276
+ each Rend through `stat_block_action_id`, and any other attack, Grapple,
277
+ Shove or spell it is commanded is refused before anything is spent. Its
278
+ allegiance is its caster's, but it never enters `party_ids` or
279
+ `encounter_ids`, and `CombatOutcome` holds no Hit Points, XP or death of its
280
+ own, though a `DeathRecord.killer_id` can name it (resolve it through
281
+ `CombatantJoined.origin_caster_id`). It leaves at 0 Hit Points — no death,
282
+ no XP — or when its caster's concentration ends; if it was the current
283
+ actor, the turn passes on once the intent or legendary action that removed
284
+ it has resolved, and a turn it left during its own intent ends with
285
+ `TurnEnded`. See the migration guide for every delta.
286
+
287
+ ### Added
288
+
289
+ - **`activities/actor_stats.py`** — the per-actor D20-test modifier projection
290
+ (SRD 5.2 §D20 Tests / §Proficiency). `save_modifier(combatant, ability)` and
291
+ `check_modifier(combatant, ability, skill=None)` return a typed
292
+ `D20Modifier(ability, ability_mod, proficiency, expertise, total)`;
293
+ `ability_modifier_of` / `proficiency_bonus_of` / `skill_ability` are the
294
+ supporting primitives.
295
+ - **`activities/d20.py`** — the single SRD 5.2 D20 Test primitive.
296
+ `roll_d20_test(rng, modifier, sources, *, forced_natural=None) -> D20Result`
297
+ with a typed `AdvantageSources(advantage, disadvantage)` and
298
+ `resolve_mode(...)` implementing the SRD's cancel-to-normal rule. Normal mode
299
+ still consumes exactly one RNG draw, so pre-0.6 seeded streams are unmoved.
300
+ - **`dnd5e_engine.AdvantageSource`** (new top-level export) — the closed
301
+ `Literal` naming *why* a roll had advantage/disadvantage (`flag`,
302
+ `condition:self`, `condition:target`, …), carried on the roll events.
303
+ - **Roll-breakdown fields on the roll events.** `AttackRolled`, `SaveRolled`,
304
+ `CheckRolled` and `ConcentrationCheck` gained `natural` (the die kept after
305
+ advantage/disadvantage), `modifier` (the flat bonus) and `sources`, plus
306
+ `advantage` on `SaveRolled` / `CheckRolled` / `ConcentrationCheck`. All
307
+ optional; hosts that ignore them are unaffected.
308
+ - **`TurnPhase(phase, actor_id, round_number)` event** — marks each
309
+ `round_start` / `turn_start` / `turn_end` boundary immediately before that
310
+ phase's hooks run. It is a marker event with no mechanical payload; renderers
311
+ should skip event types they do not recognise.
312
+ - **`turn_lifecycle.py`** — the `TurnLifecycle` hook registry
313
+ (`register` / `unregister` / `run`) that the single turn-advance path drives.
314
+ Every "at the start/end of a turn" rule registers here instead of being
315
+ open-coded into the advance path.
316
+ - **Timed effect expiry.** Effect durations in `seconds` (materialised as
317
+ `ceil(seconds / 6)` rounds), `turns`, and `flags["until_end_of_next_turn_of"]`
318
+ now expire, alongside the pre-existing `rounds` counter.
319
+ - **Proficiency fields on `PartyMemberSpec` and `Combatant`** —
320
+ `save_proficiencies`, `skill_proficiencies`, `skill_expertise`,
321
+ `weapon_proficiencies`, plus `Combatant.proficiency_bonus_override` (monsters
322
+ carry their template's CR-derived bonus). All default to empty / `None`, so a
323
+ host that sets none reproduces pre-0.6 behaviour exactly.
324
+ - **`test_capability_matrix.py::test_status_rows_match_code_probes`** pins
325
+ `docs/capabilities.md` status rows to grep-level code probes, in both
326
+ directions.
327
+ - **Conditions enforced (C12).** `rules/conditions.py` gains
328
+ `d20_test_penalty`, `exhaustion_level_of`, `project_speed`,
329
+ `conditions_block_actions`, `conditions_auto_crit_within_5ft`,
330
+ `SPEED_ZERO_CONDITIONS` and `AUTO_CRIT_WITHIN_5FT_CONDITIONS`;
331
+ `conditions_grant_advantage_on_attack` accepts keyword-only `distance_ft`,
332
+ `grappler_id` and `target_id` (the Prone and Grappled rows).
333
+ `SpatialTopology.distance_ft(a, b)` is a new Protocol method implemented by
334
+ both backends. `ActivityResolutionContext` gains `target_distance_ft`,
335
+ `attacker_grappler_id` and `d20_test_penalty`. New reasons on existing
336
+ closed sets: `IntentRejectedError` `"actor_incapacitated"`,
337
+ `MoveFailed.reason="speed_zero"`, `AttackFailed` / `CastFailed`
338
+ `"target_is_charmer"`. No new event class and no new top-level export.
339
+ - **Grid AoE targeting (C16).** `_expand_aoe_target_list` enumerates the typed
340
+ template (`sphere` / `cone` / `line` / `cube` / `cylinder`; Foundry `radius`
341
+ = emanation, `circle`, `square` mapped) via `GridTopology.cells_in_template`
342
+ and drops cells without line of effect from the point of origin (SRD 5.2
343
+ §Point of Origin). New `PlayerIntent.direction: tuple[int, int] | None`
344
+ aims cones, lines and cubes (defaults to caster → named target).
345
+ - **Creature cover.** `GridTopology.cover_between(a, b, occupied_cells=())`
346
+ — every other live combatant on the line grants half cover; blocked cells
347
+ grant total cover and block sight (`has_line_of_sight`). A `cover_cells` tag
348
+ on the **target's own** cell now counts (the origin's still never does); a
349
+ creature on a `"total"` cover cell is therefore untargetable.
350
+ - **AoE cover is measured from the point of origin** (SRD 5.2 §Cover), not from
351
+ the caster: the AoE cast path threads the resolved template origin
352
+ (`_aoe_cover_origin`) into the per-target cover map.
353
+ - **Multi-cell `move` intent — grid backend only.** `_handle_move` paths with
354
+ `GridTopology.shortest_path` (walls block, no diagonal corner-cutting,
355
+ enemies impassable, allies passable, no ending in an occupied cell) and
356
+ charges each leg's `edge_distance`; new `MoveFailed.reason` members
357
+ `unreachable`, `occupied`, `blocked_path`. The legacy zone graph keeps its
358
+ pre-0.6 single-adjacent-step contract, `not_adjacent` rejection included.
359
+ - **Forced movement.** `orchestrator.push_combatant(live, target_id,
360
+ origin_cell, distance_ft)` and the `CombatantMoved(forced=True)` event;
361
+ `activities/forced_movement.py` wires Thunderwave's push.
362
+ - **Vision & light (C16b).** `GridScene.lighting`, `default_lighting`,
363
+ `obscurement_cells` (`LightLevel` / `Obscurement` Literals) and
364
+ `GridTopology.can_see(a, b, senses)`; `ActivityResolutionContext.target_unseen`
365
+ / `attacker_unseen_by` feed the `unseen` `AdvantageSource` both directions.
366
+ - **Vision & light consumers (C16b).** A new composite predicate,
367
+ `orchestrator.py::_combatant_can_see(live, viewer, target)`, folds Blinded
368
+ (viewer; only blindsight in range sees through it) and Invisible (target;
369
+ blindsight or truesight in range, with line of sight, sees it) — never
370
+ darkvision — on top of `GridTopology.can_see`.
371
+ It now backs every SRD 5.2 "can see" conjunct outside the raw `unseen`
372
+ attack-roll row: Dodge's "if you can see the attacker" attack-disadvantage
373
+ half (both the regular-attack context sites and opportunity attacks),
374
+ Ranged Attacks in Close Combat's "enemy who can see you", the Opportunity
375
+ Attack "creature that you can see" trigger (both PC↔monster directions — a
376
+ sight-blocked reactor spends no Reaction), Hide's "out of any enemy's line
377
+ of sight" gate (skipped only when the hider's own cell already carries
378
+ Three-Quarters/Total cover), and Frightened's line-of-sight gate. The
379
+ Invisible "can somehow see you" carve-out is threaded through
380
+ `rules/conditions.py::conditions_grant_advantage_on_attack`'s new
381
+ keyword-only `attacker_invisibility_pierced` / `target_invisibility_pierced`
382
+ parameters (default `False`, preserving pre-C16b behaviour); Frightened's
383
+ attack-roll disadvantage gets a matching `fear_source_in_sight` keyword.
384
+ New `SpatialTopology.light_on_cell(cell) -> LightLevel` reports per-cell
385
+ light, so a dark cell now also satisfies Hide's "Heavily Obscured" gate
386
+ (SRD 5.2 glossary: "An area of darkness is Heavily Obscured"). New
387
+ `MoveFailed.reason` member `"frightened"`: a `"move"` intent that would
388
+ bring the mover strictly closer to a visible, known, living fear source is
389
+ now rejected (SRD 5.2 "You can't willingly move closer to the source of
390
+ fear"). `AttackRolled` gains additive `advantage_sources` /
391
+ `disadvantage_sources: list[AdvantageSource]` fields — the directional
392
+ split of `sources` — so a mutual-unseen swing now reports `["unseen"]` on
393
+ each list instead of the old `sources = ["unseen", "unseen"]` merge
394
+ (`sources` itself is unchanged).
395
+ - `SaveBlock.ignore_cover` is honoured when present (`roll_save(...,
396
+ ignore_cover=)`); the dataset field itself ships with C22.
397
+ - **C22 dataset consumers.** `Combatant.trait_mechanics` (hydrated from the
398
+ monster template's typed `special_abilities[].mechanic`): **Magic
399
+ Resistance** now rolls saving throws against spells with Advantage
400
+ (`AdvantageSource` gains `"trait"`). `roll_save(..., ignore_cover=)` +
401
+ `SaveBlock.ignore_cover` — Sacred Flame's target gets no cover bonus.
402
+ `apply_damage(..., magical=)` — a magic weapon's (`Weapon.magical`) or a
403
+ spell's Bludgeoning/Piercing/Slashing damage overcomes resistance to
404
+ *nonmagical* B/P/S; `Combatant.physical_resistances_nonmagical_only`
405
+ (also on `EncounterMemberSpec`, default `True`) makes the qualifier explicit.
406
+ Save-path target cover is hydrated for templated spell casts; when an AoE's
407
+ origin cell IS the target's cell, the new `GridTopology.cover_on_cell` reads
408
+ the tag on that cell (`cover_between` itself is unchanged — its target-cell
409
+ walk shipped with C16). Multiattacks for the five opaque-key monsters
410
+ resolve to their exact attack mix.
411
+ - **`dnd5e_engine.spellcasting` (C17)** — pure SRD 5.2 spell-slot tables and
412
+ derivations, zero I/O. `derive_spell_slots(class_slug, progression,
413
+ level)`, `derive_pact_slots(level)`, `multiclass_caster_level(classes)`,
414
+ `slots_for_caster_level(caster_level)`, `effective_caster_level(
415
+ progression, level)`, `resolve_target_count(count_formula, *,
416
+ cast_level=)` (a restricted `ast`-walker roll-data evaluator — never
417
+ `eval`), `count_scales_with_cast_level(count_formula)`,
418
+ `spell_component_metadata(spell)`, `resolve_ritual_cast(spell, *,
419
+ prepared, ritual_adept=False) -> RitualCast`, and the `SPELL_SLOT_TABLE`
420
+ / `PACT_SLOT_TABLE` constants. `derive_spell_slots`, `derive_multiclass_
421
+ slots`, `derive_pact_slots`, `resolve_ritual_cast` and `RitualCast` are
422
+ new top-level `dnd5e_engine` exports.
423
+ - **`build_spec.derive_multiclass_slots` / `derive_multiclass_pact_slots`**
424
+ — project `CharacterBuildSpec.classes` through a `loader` (explicit or the
425
+ lazy default `get_lib_loader()`) into a Spellcasting/Pact Magic pool;
426
+ `build_party_member` calls both to fill an empty `CombatInstance.
427
+ spell_slots`/`pact_slots`.
428
+ - **`PartyMemberSpec.pact_slots` / `CombatInstance.pact_slots` /
429
+ `LiveCombatView.pact_slots_by_entity`** — the Pact Magic pool, all
430
+ defaulting to `{}` and mirroring the existing `spell_slots` shape.
431
+ - **`PlayerIntent.target_ids: tuple[str, ...] | None`** — explicit
432
+ multi-target aiming for a count-scaled cast (Magic Missile darts);
433
+ `PlayerIntent.as_ritual: bool = False` — request a Ritual cast (rejected
434
+ in-combat).
435
+ - **`resolve_short_rest(..., *, pact_slots=None, pact_slot_max=None)`** and
436
+ **`resolve_long_rest(..., *, spell_slots=None, spell_slot_max=None,
437
+ pact_slots=None, pact_slot_max=None, exhaustion_level=None)`** — all
438
+ keyword-only, all default `None`. `RestOutcome` gains `spell_slots`,
439
+ `pact_slots`, `exhaustion_level`, each populated only when its matching
440
+ input pair was supplied.
441
+ - **`SpellCast` event** — `actor_id`, `spell_id`, `slot_level`, `ritual`,
442
+ `components`, `material`, `material_consumed`, `material_cost_gp`.
443
+ Emitted after the slot gate on every PC cast path (on-turn, readied
444
+ reaction, Counterspell). `CastFailedReason` gains `"ritual_in_combat"`.
445
+ - **Monster action economy surface (C18).** New events: `RechargeRolled
446
+ (monster_id, action_slug, roll, threshold, succeeded)`,
447
+ `LegendaryActionUsed(actor_id, action_slug, uses_remaining)`,
448
+ `LegendaryResistanceUsed(actor_id, uses_remaining)`. New top-level
449
+ function: `resolve_legendary_resistance(handle, entity_id) -> int`.
450
+ `advance_monster_turn` gains keyword-only `legendary: bool = False`,
451
+ `actor_id: str | None = None`. New `types.combat.MonsterActionUses`
452
+ dataclass (`recharge_spent`, `uses_remaining`). `Combatant` gains
453
+ `legendary_actions_max` / `legendary_actions_remaining` /
454
+ `legendary_resistances_max` / `legendary_resistances_remaining` /
455
+ `has_fled: bool = False` / `spellcasting_ability: str | None = None`.
456
+ `LiveCombatView` gains `monster_action_uses_by_entity`,
457
+ `legendary_actions_by_entity`, `legendary_resistances_by_entity` (a new
458
+ `views.MonsterActionUsesView` projects the first). `GridScene.sunlight:
459
+ bool = False` — a whole-scene Sunlight Sensitivity flag. All additive
460
+ with defaults reproducing pre-0.6 behaviour.
461
+ - **Class feature surface (C20).** `PlayerIntent.pool_points: int | None`,
462
+ `PlayerIntent.redeem_granted_die: GrantedDie | None`;
463
+ `PartyMemberSpec.classes: dict[str, int]`, `.feats: tuple[str, ...]`,
464
+ `.fighting_style: FightingStyle | None`; `Combatant.classes`,
465
+ `.fighting_styles`, `.worn_armor`, `.shield_equipped`,
466
+ `.flurry_strikes_remaining`, `.extra_actions_remaining`,
467
+ `.action_surge_used_this_turn`; `TurnCombatView.extra_actions_remaining`;
468
+ `types.combat.FightingStyle` / `WornArmor`;
469
+ `ActivityResolutionContext.scaling_value` / `martial_arts` / `granted_die` /
470
+ `granted_die_rolls` and the matching `build_activity_context` keywords;
471
+ `build_scale_values(classes=)`; `roll_damage_part(die_floor=)`; the pure
472
+ `rules/uses.py` (`UsesRollData`, `evaluate_uses_formula`).
473
+ `EffectExpiryReason` gains `"expended"`, `"not_extended"` and
474
+ `"incapacitated"`; `AttackFailed.reason` gains `"no_granted_die"`. All
475
+ additive, with defaults that reproduce the pre-C20 behaviour.
476
+ - **Summons foundations surface (C21a).** `PlayerIntent.form_id: str | None`,
477
+ `PlayerIntent.stat_block_action_id: str | None`; `views.ConstructView`,
478
+ `views.TransformView`, `LiveCombatView.constructs` / `.transformations`; the
479
+ pure `activities/conjuration.py` (`CONJURATION_ALLOWLIST`, `ConjurationKind`,
480
+ `TransformSource`, `ConjurationCarrier`, `ConstructRequest`,
481
+ `TransformRequest`, `ENCHANTED_WEAPON_FLAG`, `TRANSFORM_FORM_FLAG`,
482
+ `enchant_weapon`, `ConstructSpec`, `CONSTRUCTS`, `construct_attack_activity`,
483
+ `StatBlockMagnitudes`, `WildShapeTier`, `WILD_SHAPE_TIERS`,
484
+ `wild_shape_tier`, `uses_summon_roll_data`, `TransformRider`,
485
+ `TRANSFORM_RIDERS`); `ActivityResolutionContext.conjuration` /
486
+ `construct_requests` / `transform_requests` / `weapon_enchantment_to_hit` /
487
+ `stat_block_magnitudes` and the matching `build_activity_context` keywords;
488
+ `extra_flags=` on `apply_activity_effects` and
489
+ `passive_effect_to_active_effect`; `build_context.spell_attack_magnitudes`;
490
+ `monster_actions.multiattack_count`. `CastFailedReason` gains
491
+ `"invalid_form"` and `"no_spellcasting"`, `AttackFailed.reason`
492
+ `"action_unavailable"`, `EffectExpiryReason` `"temp_hp_depleted"`. All
493
+ additive, with defaults that reproduce the pre-C21a behaviour; no event type
494
+ is added and `dnd5e_engine.__all__` is unchanged.
495
+ - **Roster summons surface (C21b).** `events.CombatantJoined(entity_id, name,
496
+ stat_block_slug, origin_caster_id, spell_id, initiative_count,
497
+ after_entity_id, zone_id, hp_max, ac)`, `events.CombatantLeft(entity_id,
498
+ reason)` and `events.CombatantLeftReason` (`"concentration_drop"`,
499
+ `"spell_ended"`, `"zero_hp"`); both classes join the `CombatEvent` union and
500
+ `ALL_COMBAT_EVENT_TYPES`. `views.SummonView`, `LiveCombatView.summons`. In
501
+ `activities/conjuration.py`: `SummonSpec`, `SUMMONS`, `SummonRollData`,
502
+ `evaluate_summon_formula`, `summon_attack_count`, `summon_magnitudes`,
503
+ `SummonRequest`, the `"summon"` member of `ConjurationKind`, and
504
+ `StatBlockMagnitudes.attack_bonus` / `.attack_damage_bonus`;
505
+ `ActivityResolutionContext.summon_requests`. All additive, with defaults
506
+ that reproduce the pre-C21b behaviour; `dnd5e_engine.__all__` is unchanged.
507
+
508
+ ### Changed
509
+
510
+ - **Saving throws apply real modifiers.** All three save paths (activity saves,
511
+ the damage-triggered concentration check, the end-of-turn repeat save) now add
512
+ `ability modifier + proficiency bonus (if proficient)` for all six abilities,
513
+ where v0.5 projected DEX only and the two orchestrator paths rolled a raw d20.
514
+ Monster ability scores, save/skill proficiencies and proficiency bonus hydrate
515
+ from `monster_template_slug`.
516
+ - **Ability checks apply the actor projection** and fold the
517
+ `abilities.check` / `abilities.skill` / `abilities.<ab>.save` (and Foundry-native
518
+ `system.bonuses.abilities.*`) effect-change families, where
519
+ `build_activity_context` previously hard-coded `check_modifiers={}`.
520
+ - **Activity attack rolls honour advantage/disadvantage**: attacker
521
+ `flags.advantage.attack` / `flags.disadvantage.attack` effects plus the
522
+ condition-derived sources, cancelling per SRD §Advantage and Disadvantage.
523
+ Opportunity attacks still roll flat `normal`.
524
+ - **Every d20 in the engine goes through `roll_d20_test`** — attacks, saves,
525
+ ability/skill checks, death saves and the concentration check.
526
+ - **`ConcentrationCheck` is now emitted** for the damage-triggered concentration
527
+ save. *Transitional:* the legacy `SaveRolled(ability="con")` for the same roll
528
+ is emitted alongside it until v0.7; a host that counts saves must filter one
529
+ of the pair out.
530
+ - **`D20Result.first`** is the first die drawn, deliberately not named
531
+ `natural` — `AttackRolled.natural` is the *kept* die (the one the natural-20
532
+ crit / natural-1 fumble test reads).
533
+ - Turn advancement runs through one shared path, so every boundary rule fires
534
+ in a single, registration-ordered place.
535
+ - **The SRD 5.2 conditions now have teeth (C12)** — the Incapacitated
536
+ action/bonus/reaction gate, Speed 0, exhaustion's `-2 × level` on every D20
537
+ Test (death saves included) and `-5 ft × level` Speed, the Prone and Grappled
538
+ attack rows, auto-crit within 5 ft of a Paralyzed/Unconscious target, the
539
+ Charmed target restriction (on the monster turn path as well as the player's
540
+ — a charmed monster will not select its charmer), and Unconscious at 0 HP —
541
+ including a Character *hydrated* into combat already at 0 HP — with
542
+ massive-damage instant death plus one death-save failure per damaging hit
543
+ while down. Four SRD condition rows remain unenforced and the capability row
544
+ stays `⚠️ Partial`; see `docs/capabilities.md` and `BACKLOG.md`.
545
+ - **`conditions_grant_disadvantage_on_ability_checks` /
546
+ `project_passive_check_modifiers` changed their answer for two slugs**
547
+ (both PyPI-live): `exhaustion` no longer reports disadvantage (SRD 5.2
548
+ replaced the 2014 rule with the numeric penalty), and `frightened` now does.
549
+ See the migration guide.
550
+ - **Condition immunity is honoured by the state folds, not only the event.**
551
+ A status on a creature listed in `Combatant.condition_immunities` no longer
552
+ lands on `Combatant.conditions` or `active_conditions` — neither through the
553
+ `start_combat` effect seed nor the runtime `EffectApplied` fold, which
554
+ previously unioned it in behind the already-suppressed `ConditionApplied`.
555
+ Before C12 that was near-cosmetic; with the gates above it decided whether an
556
+ immune creature could act at all.
557
+ - **`ConditionApplied` / `ConditionRemoved` now materialise on
558
+ `Combatant.conditions`**, so a condition applied mid-combat reaches every
559
+ condition consumer (a Topple-proned target is attacked at advantage in
560
+ melee). Reviving from 0 HP leaves the creature Prone: `ConditionRemoved`
561
+ (`unconscious`) followed by `ConditionApplied` (`prone`).
562
+ - **The two orchestrator-level save paths** (the concentration check and the
563
+ end-of-turn repeat save) now honour the condition projections: STR/DEX
564
+ auto-fail, Restrained DEX disadvantage and the exhaustion penalty.
565
+ Effect-derived `passive_save_bonus` (Bless/Bane) on these two paths is
566
+ still unenforced; see `BACKLOG.md`.
567
+ - Walls now block movement as well as sight; `edge_distance` returns `None` for
568
+ an illegal step. On the grid, `ActorMoved` carries the whole intent's distance
569
+ (one event per `move`); the zone backend still emits one per step.
570
+ - **`start_combat(grid_scene=...)` raises `ValueError`** when a combatant's
571
+ `zone_id` is out of bounds or blocked, instead of silently seating them on an
572
+ unusable cell.
573
+ - **A main-hand attack keeps the turn (R1, C14)** when `attacks_remaining > 0`
574
+ after the swing, or when a two-weapon-fighting off-hand window is still
575
+ open; the turn ends only once neither condition holds. A subsequent swing
576
+ in the same Attack action no longer re-pays the Action (R2): only the
577
+ first swing hard-gates on `action_available`, and an exhausted
578
+ `attacks_remaining` on a later swing is a turn-KEEPING `AttackFailed`
579
+ rather than the "no Action" rejection every other Action-costed intent
580
+ raises. `AttackFailed` / `CheckRolled` (Hide, `escape_grapple`) /
581
+ `SaveRolled` (Grapple, Shove) / `CombatantMoved` (Shove's push option) are
582
+ new observable events on these paths; see the migration guide for the
583
+ exact keep-turn predicate and draw discipline.
584
+ - **A monster with no `monster_template_slug` now attacks.** The legacy
585
+ evaluator that used to swing template-less monsters was retired without a
586
+ replacement, so they silently passed every turn; `_synthesize_attack_from_legacy_fields`
587
+ now builds one typed `AttackActivity` per turn from the spec-level
588
+ `attack_bonus` / `damage_dice` / `damage_type` fields (unparseable dice
589
+ still no-op). Hosts with template-less monster party members will see them
590
+ start attacking; seeded streams containing a template-less monster's turn
591
+ move.
592
+ - **Weapon proficiency gates the Proficiency Bonus on an attack roll (C15).**
593
+ `Combatant.weapon_proficiencies: list[str] | None` — `None` (the field
594
+ never explicitly set on `PartyMemberSpec`) assumes proficient with every
595
+ weapon; an explicit list, even empty, is enforced by weapon category or
596
+ slug. Monsters are always proficient (never carry the field explicitly).
597
+ - **Attack range widens to three tiers (C15).** `_weapon_attack_range_ft`
598
+ returns `(normal, max)`; a shot between `normal` and `max` now resolves at
599
+ disadvantage (`"range:long"`) instead of the pre-C15 binary in-range/
600
+ rejected split, and a melee weapon carrying the Thrown property can attack
601
+ beyond its melee reach using its thrown bands instead of being rejected.
602
+ - **Ranged Attacks in Close Combat (C15).** A ranged (or effectively-ranged
603
+ thrown) attack rolls with disadvantage (`"ranged_in_melee"`) when a living,
604
+ sighted (`SpatialTopology.can_see`), non-Incapacitated hostile is within 5
605
+ ft of the attacker.
606
+ - **Heavy-property disadvantage (C15).** A wielder with a raw Strength score
607
+ below 13 rolls a Heavy weapon's attack with disadvantage.
608
+ - **Versatile grip (C15).** `PlayerIntent.two_handed: bool = False` — when
609
+ `True` and the weapon carries `versatile_damage`, a melee swing uses the
610
+ two-handed damage die instead of the one-handed die.
611
+ - **Damage attribution and crit tracking (C15).** `DamageApplied.source_id:
612
+ str | None` (weapon slug / synthesized activity id / `"mastery:<slug>"`)
613
+ and `DamageApplied.is_crit: bool = False`. A critical hit against a target
614
+ already making death saves counts as two failures instead of one (SRD
615
+ §Damage at 0 Hit Points).
616
+ - **Loading property (C15).** `Combatant.loading_weapon_fired_this_turn:
617
+ bool = False`, reset at the actor's own turn start. A second Loading-weapon
618
+ shot in the same turn is rejected pre-resolution with
619
+ `AttackFailed(reason="weapon_already_fired")`, gated ahead of the Charmed
620
+ target gate.
621
+ - **All eight 2024 weapon masteries (C15).** `activities/mastery.py`
622
+ resolves Graze (flat governing-ability-mod damage on a miss) and Topple
623
+ (Constitution save vs. `8 + proficiency + governing-ability mod`, prone on
624
+ a failure, gated by the shared `is_condition_immune` helper) directly.
625
+ Vex, Sap, Slow, Push, Cleave and Nick report through
626
+ `ActivityResolutionContext.mastery_procs`, which the orchestrator folds
627
+ into live combat state: Vex grants Advantage against the same target for
628
+ the attacker's next 2 attack-roll turns (one-use); Sap marks the target
629
+ with Disadvantage until the attacker's next turn start (one-use); Slow
630
+ applies a flat, non-stacking −10 ft Speed penalty cleared at the source's
631
+ next turn start; Push forces a full 10 ft move via `push_combatant`
632
+ (controller ruling: always the full distance; the "Large or smaller" size
633
+ gate is unmodelled); Cleave chains one extra attack+damage roll against the
634
+ nearest eligible living hostile within 5 ft of the first target and within
635
+ reach (deterministic tie-break by `entity_id`, once per turn, no re-proc);
636
+ Nick's off-hand extra attack spends no Bonus Action and does not require
637
+ one — `_offhand_window_open` now keeps the turn open after any Light
638
+ main-hand swing even when the Bonus Action is already spent, so a host
639
+ must submit `"pass"` to end such a turn (see the migration guide).
640
+ - **`test_capability_matrix.py`** gains six probes pinning the C15 rows in
641
+ `docs/capabilities.md` (attack rolls, conditions, forced movement, weapon
642
+ mastery).
643
+ - **A count-scaled cast now emits N `DamageApplied`, not one (C17).** A
644
+ `damage`-kind activity whose `target.affects.count` formula references
645
+ `@item.level` (Magic Missile) resolves N separate `DamageApplied` events
646
+ sharing one rolled damage instance — 3 darts at slot level 1, +1 per slot
647
+ level above 1. Total damage now scales ×N for every such cast; the draw
648
+ count is unchanged (shared roll, applied N times). Targeting more
649
+ entities than the resolved count, or an unknown `target_ids` entry, is
650
+ rejected pre-slot with `CastFailed(reason="target_invalid")`.
651
+ - **An armed Counterspell/readied-cast reaction with no slot at its readied
652
+ level is now skipped, not fired (C17).** `_pop_pending_reaction` gained
653
+ an `eligible=` predicate; an ineligible reactor's reaction stays queued
654
+ and its Reaction/slot are untouched. Counterspell is additionally gated
655
+ on its own 60 ft range with line of sight (`_in_range_with_los`) — an
656
+ out-of-range or LoS-blocked reactor's Counterspell is likewise skipped.
657
+ - **`build_party_member` fills an empty `spell_slots`/`pact_slots` from
658
+ `CharacterBuildSpec.classes` (C17).** Previously an empty
659
+ `CombatInstance.spell_slots` was copied verbatim (a caster with no
660
+ slots); now an empty (falsy) pool falls back to `derive_multiclass_slots`
661
+ / `derive_multiclass_pact_slots`. A host that relied on an empty
662
+ `spell_slots` dict to make every cast fail with `CastFailed(reason=
663
+ "no_slot")` will now see real, derived slots instead. A non-empty pool
664
+ is untouched.
665
+ - **`CharacterBuildSpec.classes: dict[str, int]`** is the multiclass
666
+ carrier (C17), reconciled with the single-class `class_slug`/`level`
667
+ fields by a `model_validator(mode="before")`; `model_copy(update=...)`
668
+ bypasses that validator (Pydantic does not re-run `mode="before"` on
669
+ `model_copy`), so changing only one of `level`/`classes` via `model_copy`
670
+ can desync them — construct a fresh instance instead.
671
+ - **Pack Tactics applies to a host-driven attacker (C21a).** SRD 5.2: "The
672
+ tough has Advantage on an attack roll against a creature if at least one of
673
+ the tough's allies is within 5 feet of the creature and the ally doesn't
674
+ have the Incapacitated condition." The trait resolved only on a monster's
675
+ own `advance_monster_turn`; an attack through `submit_player_intent` by a
676
+ creature carrying it (a template NPC or monster the host drives: a Tough, a
677
+ Hobgoblin Warrior, a Kobold Warrior, a Wolf) now rolls with Advantage too,
678
+ one more d20, whenever such an ally stands within 5 feet of the target. A
679
+ seeded stream that has one moves from that roll on.
680
+ - **Summon Dragon seats a creature, or is refused before anything is spent
681
+ (C21b).** The cast used to spend its slot and Action and anchor its
682
+ concentration, with nothing else. With no legal space it is now refused with
683
+ `CastFailed` — `target_invalid` for a named cell that is off the map,
684
+ blocked, occupied or not the grid's own `col,row` id, `out_of_range` for one
685
+ beyond range or out of sight, when no cell in range is free, or when the
686
+ caster is Blinded (unless its Blindsight reaches the space) —
687
+ and a `ready` naming it with `target_invalid`.
688
+ - **Allegiance reads a summon's caster (C21b).** The monster AI picks its
689
+ lowest-HP target among every enemy (`_is_enemy`), a summon included, instead
690
+ of among `party_ids`; Help, Cleave, Sneak Attack and Pack Tactics adjacency,
691
+ the ranged attack's close-combat check, Hide and grid movement treat a
692
+ summon as its caster's ally. For a roster without a summon each reads as
693
+ before.
694
+ - **Monster turn-start mechanics are keyed on the creature (C21b).** The
695
+ legendary action reset, recharge rolls and regeneration run once per round
696
+ and creature rather than per round and turn index, so a removal mid-round
697
+ neither skips nor repeats them. Unchanged while the roster never changes.
698
+ - **`ActiveEffect.statuses` serializes sorted (C23).** The field stays a
699
+ `set[str]`; every dump mode emits a sorted list, so a multi-status
700
+ effect's JSON no longer follows the process's hash seed.
701
+
702
+ ### Fixed
703
+
704
+ - **A `PartyMemberSpec` that never set `attack_bonus` was pinned to a 0
705
+ to-hit bonus (2026-09-02, C15 Task 1).** `Combatant.attack_bonus` widens
706
+ from `int = 0` to `int | None = None`; `None` (the host never explicitly
707
+ set `PartyMemberSpec.attack_bonus`) now correctly falls through to the
708
+ real governing-ability-modifier + proficiency-bonus computation instead of
709
+ being silently treated as a genuine `0` override. A host-supplied value
710
+ (including every monster's, always threaded as a concrete int) is
711
+ unaffected — byte-identical to every pre-C15 fixture. The engine's own
712
+ `build_party.py` host-party-building path always sets `attack_bonus`
713
+ explicitly (from the pre-computed character sheet), so this fix does not
714
+ change its output — pre-existing behaviour there, unchanged.
715
+ - **Lay on Hands' Heal no longer crashes (C20).** Its `@scaling` healing
716
+ formula raised `ValueError: Unhandled roll-data token` out of
717
+ `submit_player_intent` after the Bonus Action was spent; `@scaling` now
718
+ resolves to the points drawn.
719
+ - **The free Patient Defense no longer spends a Focus Point (C20).** A feature
720
+ activity is charged its own `consumption.targets` cost, and one that declares
721
+ none is free when a sibling activity of the same feature declares one.
722
+ - **An Incapacitated condition an effect imposes now breaks concentration and
723
+ releases grapples (C20).** Hold Person's Paralyzed, or any effect status that
724
+ is Incapacitated or implies it, left the creature concentrating and
725
+ grappling: the effect's fold wrote the condition onto the combatant first,
726
+ so the `ConditionApplied` fold that runs those consequences found it already
727
+ there and skipped them. Whichever fold writes the condition first now runs
728
+ them once, Rage's end included; for an effect, their events follow its
729
+ `EffectApplied`.
730
+ - **A readied concentration spell now concentrates (C21a).** A readied cast
731
+ ran none of the concentration bookkeeping, so it never started
732
+ concentration, never ended an earlier one and never made later damage draw
733
+ a Constitution save.
734
+ - **A stat-block on-hit trait's escape DC no longer crashes (C21a).**
735
+ `activities/formula.py` had no handler for `@skills.<code>.passive` — the
736
+ attacker's own passive score for a Foundry skill code, e.g. the Crocodile's
737
+ Bite: "the target ... has the Grappled condition (escape DC 12)" — so any
738
+ caster hitting the token, transformed or not, raised `ValueError: Unhandled
739
+ roll-data token`. It now resolves `10 + ability modifier + proficiency bonus
740
+ (doubled with Expertise) when the caster is proficient in that skill`: a
741
+ transformed creature's form gives the SRD escape DC (a Crocodile's 12), while
742
+ a template monster's uniform `attack_bonus` numbers give `10 + attack_bonus`
743
+ (BACKLOG).
744
+ - **A save action whose DC names an ability no longer crashes (C21a).**
745
+ `activities/save.py` resolved only the `spellcasting` and `flat` DC
746
+ calculations, so a save resolved without a fixed DC — a transformed
747
+ creature's stat-block action, a feature's save through `use_feature` —
748
+ raised `ValueError` on an ability-code DC (a Giant Spider's Web, a
749
+ Dragonborn's Breath Weapon) or a bare formula (a Swarm of Ravens'
750
+ Cacophony), and `@mod` in a monster save action's damage raised on the
751
+ monster's own turn. The DC is now 8 + that ability's modifier + the
752
+ Proficiency Bonus, or the formula, and `@mod` reads the ability the DC names
753
+ (Giant Constrictor Snake Constrict: 2d8 + 4).
754
+ - **Ended combats no longer stay in memory for the life of the process
755
+ (C23).** `end_combat` keeps the 64 most recently ended combats readable
756
+ (a repeat close, `get_live`, `drain_pending_events`, `narration_events`)
757
+ and releases older ones; their handles raise `UnknownHandleError`. A live
758
+ combat, or a new one registered under a reused handle id, is never
759
+ released.
760
+ - **Truesight no longer lets a Blinded creature see (C23).** SRD 5.2 lets
761
+ only Blindsight work "even if you have the Blinded condition"; the "can
762
+ see" composite and Summon Dragon's placement now agree. Truesight still
763
+ sees an Invisible creature.
764
+ - **A host-moved foe no longer draws opportunity attacks from its own side
765
+ (C23).** The monster-reactor direction takes only the mover's foes.
766
+ - **An engine-rolled Initiative reads a template's Dexterity (C23).** An
767
+ encounter member with a resolvable `monster_template_slug` and
768
+ `dexterity` left at 10 adds the template's modifier, the Dexterity it
769
+ fights with.
770
+ - **`nat20-bridge` reports its own version (C23).** `/v1/health` said
771
+ `"0.3.2"` since 0.3.2; `nat20_bridge.__version__` now reads the installed
772
+ package.
773
+ - **`GET /v1/combat/{cid}` returns a `grid` block.** The response now
774
+ carries the combat's `GridScene`, or `null` for a zone-graph combat — the
775
+ SillyTavern extension's map view reads it.
776
+
777
+ ### Deprecated
778
+
779
+ - `start_combat(scene_zones=...)` — `DeprecationWarning` since 0.6.0, removed in
780
+ 0.7.0. Pass `grid_scene=GridScene(...)` instead.
781
+
782
+ ## [0.5.0]
783
+
784
+ Lockstep release with `dnd5e-srd-data` 0.5.0 and `nat20-bridge` 0.5.0.
785
+
786
+ Removes the legacy (Gen 1) rules surface deprecated in 0.4.0. The engine now
787
+ ships exactly one, seedable, documented rules implementation
788
+ (`orchestrator` + `activities/`). See `docs/migration/v0.4-to-v0.5.md`.
789
+
790
+ ### Removed
791
+
792
+ - Modules `dispatch`, `event_dicts`, `types.dice`, `types.intent`, and
793
+ `rules.{combat, combat_data, combat_helpers, equipment, gambits, resolution,
794
+ spells}` (plus the private `rules._parsing` / `rules._class_meta`).
795
+ - `dnd5e_engine.ActionType`, `CombatNPC`, and the lazy
796
+ `dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
797
+ re-exports; the 0.4.0 `DeathSaveState` / `BehaviorProfile` re-export shims.
798
+ - The cross-generation parity test; its Gen 2 assertions live on in
799
+ `tests/test_attack_classification.py`.
800
+
801
+ ## [0.4.0]
802
+
803
+ Lockstep release with `dnd5e-srd-data` 0.4.0 and `nat20-bridge` 0.4.0.
804
+
805
+ Deprecates the legacy (Gen 1) rules surface. **Nothing is removed**; every
806
+ deprecated import warns. Removal lands in 0.5.0 — see
807
+ `docs/migration/v0.3-to-v0.4.md`.
808
+
809
+ ### Deprecated
810
+
811
+ - **The legacy (Gen 1) rules surface — removed in 0.5.0.** `dispatch`,
812
+ `event_dicts`, `types.dice`, `types.intent`, and `rules.{combat, combat_data,
813
+ combat_helpers, equipment, gambits, resolution, spells}` emit a
814
+ `DeprecationWarning` on import. The top-level `dnd5e_engine.ActionType` and
815
+ `dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
816
+ re-exports now resolve lazily and warn on access. Every symbol's supported
817
+ route is in `docs/migration/v0.3-to-v0.4.md`. Rationale: two drifting
818
+ implementations of the same SRD rules, a non-seedable legacy path, and host
819
+ transport/persistence shapes inside a zero-I/O package.
820
+
821
+ ### Changed
822
+
823
+ - `DeathSaveState` canonical home is now `dnd5e_engine.death_saves`;
824
+ `BehaviorProfile` is now `dnd5e_engine.types.combat`. The old locations
825
+ re-export until 0.5.0. The supported engine's import tree no longer reaches
826
+ any legacy module (pinned by `tests/test_gen2_has_no_gen1_imports.py`).
827
+
828
+ ### Added
829
+
830
+ - Cross-generation attack-roll parity test
831
+ (`tests/test_generation_parity_attack.py`): `rules/combat.attack_roll` /
832
+ `resolve_player_attack` and `activities/attack.py` are fed the same d20
833
+ stream and must keep the same natural die and agree on hit / crit / miss
834
+ under advantage, disadvantage and flat rolls. This is the guard that would
835
+ have caught the drift fixed in 0.3.1 (#11). No runtime behavior change.
836
+
837
+ ## [0.3.2]
838
+
839
+ Lockstep release with `dnd5e-srd-data` 0.3.2 and `nat20-bridge` 0.3.2.
840
+
841
+ Additive maturity pass from an independent review of the packages as a
842
+ standalone engine. **No public name is removed or changed shape.**
843
+
844
+ ### Fixed
845
+
846
+ - **Multiattack fan-out resolved the wrong attack mix for most monsters.** The
847
+ prose parser only matched `[[/item .id]]{label}`, but the corpus mostly ships
848
+ `[[/item Name]]` or bare mnemonic ids, so 174 of 180 multiattacks fell back to
849
+ "repeat one sibling N times" — silently wrong for every heterogeneous
850
+ multiattacker. The parser now reads per-token counts from the multiattack's
851
+ first sentence (excluding "It can replace one attack with …" riders) and
852
+ recovers names from Foundry's mnemonic ids. Precise joins: **6/180 → 119/180**.
853
+ Chuul, Otyugh, Cloaker, Unicorn, Mummy, Pit Fiend, Xorn and Marilith now make
854
+ their correct attack sequences. **Behaviour change for consumers:** affected
855
+ monsters deal their correct (higher) damage per round and consume more of the
856
+ dice stream, so combat transcripts seeded against 0.3.x will diverge.
857
+ - `__version__` now derives from installed package metadata instead of a
858
+ hand-maintained literal, so it cannot drift from `pyproject.toml` (0.3.0 shipped
859
+ reporting `0.2.0`). Pinned by `tests/test_determinism_contract.py`.
860
+
861
+ ### Added
862
+
863
+ - **`CheckSpec.rng`** — `resolve_check` previously drew its d20 from the
864
+ process-global `random` module with no way to inject a generator,
865
+ contradicting the documented determinism guarantee. Pass a seeded
866
+ `random.Random` for a reproducible standalone check; `None` preserves the old
867
+ behaviour. `rules.dice`, `rules.skills` and `roll_dice_str` /
868
+ `apply_changes_to_check` gain matching optional `rng` parameters. Combat
869
+ resolution was already seeded and is unchanged.
870
+ - **`docs/capabilities.md`** — a per-mechanic matrix of what the engine
871
+ resolves, what loads but produces no events, and what is not modelled. Its
872
+ published counts are recomputed from the corpus by
873
+ `tests/test_capability_matrix.py`.
874
+ - Concept pages for reactions (the pre-armed model) and monsters (the built-in
875
+ AI, multiattack join rate, absent legendary actions).
876
+ - `tests/test_docstrings_are_host_agnostic.py` — fails the build on a docstring
877
+ that names a private downstream application, retired tooling, or a doc path
878
+ that does not exist in this repo. 104 such references were rendered onto the
879
+ public API site; all removed.
880
+
881
+ ### Changed
882
+
883
+ - `CONDITION_EFFECTS[EXHAUSTION]` / `[PRONE]` descriptive strings corrected from
884
+ 2014 to SRD 5.2 wording and labelled as not-enforced; the enforced exhaustion
885
+ rule is still the 2014 one (tracked in `BACKLOG.md`).
886
+ - Sphinx cross-reference roles in docstrings, which rendered as literal markup
887
+ on the API site, converted to inline code.
888
+ - Monkeypatching `roll_dice_str` now requires a two-parameter double
889
+ (`lambda expr, rng=None: …`).
890
+
891
+ ## [0.3.0]
892
+
893
+ Lockstep release with `dnd5e-srd-data` 0.3.0 — the item charge lifecycle: gate
894
+ → spend → cast-delegate → upcast → recharge → observe. See
895
+ `docs/migration/v0.2-to-v0.3.md` for the full, host-facing migration guide.
896
+ The engine now depends on `dnd5e-srd-data>=0.3.0` (the charge-gate and
897
+ cast-delegation paths read the new `Item.uses` / `Spell.foundry_uuid` schema).
898
+
899
+ ### Added
900
+
901
+ - **Item charges** — `use_item` now validates and spends `consumption.targets[type=itemUses]`
902
+ costs against the item's `uses.max` pool, tracked in the `custom_counters` sidecar under
903
+ `item_use:<slug>` (`{"spent": n}`). Exhausted pools reject with
904
+ `CastFailed(reason="no_charges_remaining")` before the action budget is touched. Items
905
+ without a pool are unaffected. `CastFailedReason` also gains `invalid_charge_spend`, emitted
906
+ when a `charges_to_spend` request violates the activity's `consumption.scaling` (see
907
+ "Charge upcasting" below).
908
+ - **Item recharge** — `dnd5e_engine.rest.recover_item_uses` (+ `ITEM_USE_COUNTER_PREFIX`,
909
+ submodule-only export): pure recharge over `item_use:` pools. The engine's own
910
+ `rest.RecoveryPeriod` gains `"dawn"`/`"day"`/`"dusk"` (previously just `"sr"`/`"lr"`;
911
+ the data package's `RecoveryRule` already knew `"day"` — this widens the engine's
912
+ narrower rest-period literal to match); formula recovery now rolls dice ("1d6 + 1")
913
+ through an optional `rng` (also available on `recover_feature_uses`).
914
+ - **Cast delegation** — a used item's `CastActivity` now resolves its referenced spell
915
+ (uuid→Spell via `dnd5e-srd-data`'s `get_spell_by_uuid`) on the PC-intent path, honoring
916
+ the item's flat DC / attack / level overrides. `cast_spell_unresolved` now signals a
917
+ genuinely missing uuid, not missing plumbing.
918
+ - **Charge upcasting** — `PlayerIntent.charges_to_spend` casts at
919
+ `base level + extra charges`, validated against `consumption.scaling`
920
+ (`invalid_charge_spend` on violation). New `ActivityResolutionContext.cast_level_override`.
921
+ - **Charge read path** — `LiveCombatView.custom_counters_by_entity` (three-level snapshot
922
+ copy): hosts mirror `item_use:` / `feature_use:` pools per turn the same way they mirror
923
+ `spell_slots_by_entity`.
924
+
925
+ ## [0.2.0]
926
+
927
+ Lockstep release with `dnd5e-srd-data` 0.2.0 — the outcome of the gap-closing
928
+ campaign (ten scenario clusters). See `docs/migration/v0.1-to-v0.2.md` for the
929
+ full, host-facing migration guide. The engine now depends on
930
+ `dnd5e-srd-data>=0.2.0` (the rest-cap path reads the new `Feature.uses` schema).
931
+
932
+ ### Added
933
+
934
+ - **Rest & recovery** — new public, zero-I/O module `dnd5e_engine.rest`, called
935
+ *between* combats: `resolve_short_rest`, `resolve_long_rest`,
936
+ `recover_feature_uses`, the `HitDicePool` / `RestOutcome` value types, the
937
+ `RecoveryPeriod` literal — all added to the top-level `__all__`.
938
+ `FEATURE_USE_COUNTER_PREFIX` is also public, but is exported from the
939
+ submodule only: `from dnd5e_engine.rest import FEATURE_USE_COUNTER_PREFIX`.
940
+ - **Spatial (grid) surface** — `WallSegment` value type (also a top-level
941
+ export); `GridScene.wall_segments`, `GridScene.cover_cells`, and
942
+ `GridScene.difficult_terrain_cells` fields (all default empty/off);
943
+ `SpatialTopology.cover_between(...)`; and `GridTopology.cells_in_template(...)`
944
+ for sphere/cone/line AoE templates.
945
+ - **Passive-stat fields** — `condition_immunities` and `damage_vulnerabilities`
946
+ on `PartyMemberSpec` / `EncounterMemberSpec` / `Combatant`, and
947
+ `movement_modes` (`CombatantMovementModes`) plus `walk_speed_bonus` on
948
+ `PartyMemberSpec` / `Combatant`. All default empty and only take effect when
949
+ the backing dataset carries the trait.
950
+ - **Intent field** `PlayerIntent.activity_id` (default `None`) — disambiguates a
951
+ `USE_FEATURE` invocation of a multi-activity feature (Channel Divinity,
952
+ Cunning Strike).
953
+ - `PartyMemberSpec.reach_ft` (default `5`) — threads onto the live
954
+ `Combatant.melee_reach_ft`, making a reach weapon's opportunity-attack reach
955
+ reachable from the boundary spec.
956
+ - **Reaction surface** — `"ready"` intents now register a pre-armed reaction;
957
+ `PlayerIntent.reaction_trigger` narrows to the `ReactionTrigger` literal.
958
+ - `CastFailedReason` gained the values `"countered"` (Counterspell) and
959
+ `"no_uses_remaining"` (a capped, rest-recharged feature invoked over its cap).
960
+ - New additive `ActivityResolutionContext` sidecar fields backing the above
961
+ (cover, per-category damage-bonus buckets, Sneak Attack gates, reaction AC
962
+ bonus) — all default empty.
963
+
964
+ ### Changed
965
+
966
+ - **Spell save DC is now the real SRD 5.2 formula** (`8 + proficiency +
967
+ spellcasting-ability modifier`) for PC caster spells, replacing a flat
968
+ Avrae-era approximation — the campaign's widest-blast-radius change. Any host
969
+ casting a PC save-DC spell sees a different, SRD-correct DC. Item casts and
970
+ monster casters are unchanged.
971
+ - **Sneak Attack rider** — a qualifying finesse/ranged rogue hit now deals the
972
+ `@scale`-resolved extra damage, doubled on a critical hit.
973
+ - **Passive-projection behavioral flips** — Rage damage resistance now halves
974
+ matching damage; an immune condition never attaches; a matching damage
975
+ vulnerability doubles the hit; movement-mode / walk-speed bonuses (Fast
976
+ Movement, Roving) now fold onto `base_speed`.
977
+ - **Monster behavior** — fleeing monsters now retreat away from their nearest
978
+ threat instead of standing still; a mixed melee+ranged multiattack now fires
979
+ the weapon that fits the current distance instead of always the first-listed
980
+ one; monsters Dash to close an out-of-reach movement gap.
981
+ - **Reactions & action economy** — a countered `cast_spell` does **not** expend
982
+ its spell slot; a reaction-applied 1-round buff (Shield's +5 AC) now survives
983
+ until the owner's next turn; `"disengage"` is now a real turn-non-ending
984
+ action; PCs moving out of a monster's reach now provoke opportunity attacks.
985
+ - **Active-effect changes** — `multiply` / `upgrade` / `downgrade` change modes
986
+ now apply; the `rwak` / `msak` / `rsak` damage and `spell.dc` bonus buckets
987
+ now fold; a weapon-tagged `damage.bonus` change now reaches swing damage.
988
+ - Feature-owned `@scale` ids now resolve; the Foundry `(@scale.x)dN` dice-count
989
+ idiom now normalizes; capped features (Second Wind) are now per-rest limited.
990
+
991
+ ### Removed
992
+
993
+ - `ActionType.SHORT_REST` (`dnd5e_engine.types.intent`) — a dead, unreachable
994
+ member of the legacy dispatch enum with no handler. It was never the
995
+ live-combat intent surface (`events.py::IntentType` has no `"short_rest"`).
996
+ Hosts wanting rest resolution use the new `dnd5e_engine.rest` module.
997
+ - `rules/gambits.select_action` (with the private helpers `_PASS_ACTION` /
998
+ `_get_alive_targets` only it used) — the legacy per-profile gambit AI the live
999
+ monster-turn path never called. Every behaviour it offered now lives in
1000
+ `advance_monster_turn` / `activities/monster_actions.py`. Verified caller-free;
1001
+ it was not a top-level `__all__` export.
1002
+ (`rules/gambits.assign_behavior_profile` was reviewed and **retained** — it is
1003
+ a host-facing utility for constructing `EncounterMemberSpec.behavior_profile`
1004
+ from raw monster stats, not dead code.)
1005
+ ## [0.1.2]
1006
+
1007
+ ### Added
1008
+ - `ActionType.USE_ITEM` parser-facing action member.
1009
+
1010
+ ## [0.1.1]
1011
+
1012
+ ### Added
1013
+ - `LiveCombatView` — public read-model projection of live combat state.
1014
+ - `get_live(handle) -> LiveCombatView` is now part of the public API (`__all__`);
1015
+ it returns a point-in-time snapshot view instead of the private `_LiveCombat`.
1016
+ - `roll_dice_str` — the dice-expression evaluator is now a public test seam
1017
+ (renamed from the private `_roll_dice_str`).
1018
+
1019
+ ## [0.1.0]
1020
+
1021
+ First public release.
1022
+
1023
+ ### Added
1024
+
1025
+ - Pure-Python, host-agnostic D&D 5e SRD rules engine: combat orchestration,
1026
+ ability checks and saving throws, and combat-scoped effects.
1027
+ - SRD 5.2 (2024) content resolution against the typed-Activity corpus via
1028
+ `BundledAssetLoader`, reading the canonical dataset from the
1029
+ `dnd5e-srd-data` package.
1030
+ - Curated public API surface (`__all__`) with a surface guard test.
1031
+ - `py.typed` marker — the package ships inline type information.
1032
+ - Zero-I/O guarantee: no DB, network, or async dependencies in the engine.